Commit 7b3710c70d6 for woocommerce
commit 7b3710c70d627f302e05b19866a3b93db31846e9
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Mon Sep 7 09:07:23 2026 +0200
Extract the dual API engine to the WooCommerce Dual API plugin (#68287)
* Remove the WooCommerce dual API engine, docs and PoC API:
- bin/api-builder
- lib/packages/GraphQL
- src/Api
- src/Internal/Api
- test/php/src/Api
- test/php/src/Internal/Api
- test/php/src/Internal/LegacyPhpApi/SettingsTests.php
- docs/apis/dual-api (except the root README)
* Adjust code/docs for the removal of the dual API:
- Remove registration from class-woocommerce.php
- Remove webonyx package references from lib/composer.json/.lock
- Remove feature from FeaturesController.php
- Remove maybe_announce_skipped_graphql_tests from tests bootstrap
- Adjust distignore, composer.json, package.json, phpcs.xml,
phpstan.neon, phpunit.xml to remove references to the dual API
* Add a stub GraphQLController
This is needed so that on a site that upgrades from 10.9-11.1 to 11.2+
and has the dual API plugin installed, the GraphQLController class is
present in the autoloader and can be instantiated by the old code
when the REST server boots later in the same request (same approach as #67136).
* Add changelog file
* Remove dual API tests added on trunk after the engine removal
diff --git a/.github/workflows/api-staleness.yml b/.github/workflows/api-staleness.yml
deleted file mode 100644
index f9312e2530b..00000000000
--- a/.github/workflows/api-staleness.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: 'GraphQL API Staleness Check'
-
-on:
- pull_request:
- paths:
- - 'plugins/woocommerce/src/Api/**'
- - 'plugins/woocommerce/src/Internal/Api/Autogenerated/**'
- - 'plugins/woocommerce/bin/api-builder/**'
- - '.github/workflows/api-staleness.yml'
- push:
- paths:
- - 'plugins/woocommerce/src/Api/**'
- - 'plugins/woocommerce/src/Internal/Api/Autogenerated/**'
- - 'plugins/woocommerce/bin/api-builder/**'
- - '.github/workflows/api-staleness.yml'
- branches:
- - 'trunk'
- - 'release/*'
-
-concurrency:
- group: api-staleness-${{ github.event_name == 'push' && github.run_id || github.event_name }}-${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- api-staleness:
- name: 'GraphQL API Staleness Check'
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - uses: 'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0' # v7.0.0
- name: 'Checkout'
-
- - uses: './.github/actions/setup-woocommerce-monorepo'
- id: 'setup-monorepo'
- with:
- install: '@woocommerce/plugin-woocommerce...'
- pull-package-deps: '@woocommerce/plugin-woocommerce'
- php-version: '8.4'
-
- - name: 'Check GraphQL API staleness'
- run: pnpm --filter='@woocommerce/plugin-woocommerce' build:api:check
diff --git a/docs/apis/README.md b/docs/apis/README.md
index 86e39b90049..cedc3f1188e 100644
--- a/docs/apis/README.md
+++ b/docs/apis/README.md
@@ -22,7 +22,7 @@ Explore the [Store API](./store-api/README.md) documentation.
## Dual API (code + GraphQL, experimental)
-The dual API is an experimental, code-first API: you write plain PHP classes (the code API) and a build script generates a matching GraphQL endpoint from them. WooCommerce core ships its own dual API, and the underlying infrastructure can be reused by plugins to build their own.
+The dual API is an experimental, code-first API architecture: you write plain PHP classes (the code API) and a build script generates a matching GraphQL endpoint from them. The engine is provided by the WooCommerce Dual API plugin, and any plugin can use it to build its own dual API.
Explore the [Dual API](./dual-api/README.md) documentation.
diff --git a/docs/apis/dual-api/README.md b/docs/apis/dual-api/README.md
index d2e86757d0d..9ad54be93e3 100644
--- a/docs/apis/dual-api/README.md
+++ b/docs/apis/dual-api/README.md
@@ -6,60 +6,22 @@ sidebar_position: 0
# WooCommerce Dual API
-The **dual API** is a code-first API architecture: you write plain PHP classes (the **code API**), and a build script generates a fully functional **GraphQL API** that mirrors them. The two are kept in sync from a single, manually maintained source (the code API) so there is one place to add behavior and two ways to consume it (in-process PHP calls and GraphQL-over-HTTP).
+The **dual API** is a code-first API architecture: you write plain PHP classes (the **code API**), and a build script generates a fully functional **GraphQL API** that mirrors them.
-WooCommerce core ships its own dual API, but the underlying infrastructure is reusable: a plugin can define its own code API and get a matching GraphQL endpoint with the same tooling.
-
-## Status: experimental, and a proof of concept
+The dual API was introduced as an experimental feature in WooCommerce core 10.9, but as of WooCommerce 11.2 its engine has moved to a dedicated plugin, [WooCommerce Dual API](https://github.com/woocommerce/woocommerce-dual-api), which any plugin can use to build its own dual API.
> **This feature is experimental.** Everything under the `Automattic\WooCommerce\Api` namespace can change in backwards-incompatible ways, or be removed, in any release. Do not use it in production extensions.
-There are two separate parts to understand:
+## Where the documentation lives
-- **The infrastructure** (the build tooling, attributes, authorization model, engine-decoupling layer): Implementing a robust and stable infrastructure has been for now the main focus of the development efforts.
-- **WooCommerce core's own code API** (the `coupons` and `products` queries/mutations): This is a **proof of concept**. It exists to exercise the infrastructure and will likely change significantly or be replaced in the short term. Treat it as an example, not a contract.
+The engine and its documentation live in the plugin's repository:
-This dual API, both the infrastructure and the proof of concept code API, has been introduced as an experimental feature in WooCommerce 10.9.
+- **Plugin**: [woocommerce/woocommerce-dual-api](https://github.com/woocommerce/woocommerce-dual-api)
+- **Documentation**: the [`docs/` directory](https://github.com/woocommerce/woocommerce-dual-api/tree/trunk/docs) of that repository
+- **Working example**: [woocommerce/woocommerce-simple-events](https://github.com/woocommerce/woocommerce-simple-events), a runnable reference plugin that exercises the engine end to end
## Requirements
-- **PHP 8.1+.** The code API uses enums, named arguments, and PHP 8 attributes. On PHP 8.0 or older the GraphQL endpoint is not registered.
-- **The `dual_code_graphql_api` feature flag.** It is hidden (not shown on the Features settings page). Enable it with:
-
- ```bash
- wp option update woocommerce_feature_dual_code_graphql_api_enabled yes
- ```
-
-When the flag is off, no GraphQL route is registered. This gates **every** dual-API endpoint, the one in WooCommerce core **and** any registered by plugins. Code that touches the code API classes directly should guard on `FeaturesUtil::feature_is_enabled( 'dual_code_graphql_api' )`. The settings and filters are likewise site-wide and shared across all dual-API endpoints (see [Settings and caching](./caching-and-settings.md#scope-what-applies-where)).
-
-## Which document do I need?
-
-| Your question | Start here |
-| --- | --- |
-| What is this and how does it fit together? | [Architecture](./architecture.md) |
-| How do I add to or change WooCommerce's code API? | [Extending the code API](./extending-the-code-api.md) |
-| How do I paginate a list query? | [Relay-style pagination](./pagination.md) |
-| How do I build my own dual API in a plugin? | [Creating a dual API in a plugin](./creating-a-dual-api-in-a-plugin.md) |
-| How does authentication and authorization work? | [Authentication and authorization](./authentication-and-authorization.md) |
-| How do I attach and query schema metadata? | [Metadata and discovery](./metadata.md) |
-| How do I configure the endpoint and caching? | [Settings and caching](./caching-and-settings.md) |
-| How do I regenerate the GraphQL code, and what is the staleness check? | [Building and staleness checks](./building-and-staleness.md) |
-| The infrastructure or the builder is missing something, how do I change it safely? | [Extending the infrastructure](./extending-the-infrastructure.md) |
-
-Reference material (lookup tables, exact signatures):
-
-- [Recognized directories](./reference/directories.md)
-- [Attributes](./reference/attributes.md)
-- [Recognized methods and parameters](./reference/recognized-methods-and-parameters.md)
-- [Infrastructure classes](./reference/infrastructure-classes.md)
-- [Exceptions](./reference/exceptions.md)
-
-## Audience
-
-The primary audience for this documentation is **maintainers of WooCommerce's code API** and **developers building their own dual API in a plugin**. The secondary audience is **maintainers of the dual-API infrastructure** itself.
-
-Throughout these docs, rules introduced as "in a plugin" generally apply equally when extending WooCommerce core's own code API; where a rule is core-only or plugin-only, that is called out explicitly.
-
-## A working example
+WooCommerce 11.2 or newer on PHP 8.1+, with the WooCommerce Dual API plugin installed and active. There is no feature flag: activating the plugin enables the engine.
-The [`woocommerce-simple-events`](https://github.com/woocommerce/woocommerce-simple-events) plugin is a runnable reference that exercises the infrastructure end to end: custom authentication, custom authorization attributes, granular field-level gates, pagination, scalars, and more. These docs link to it for complete, copy-pasteable examples.
+WooCommerce 10.9 to 11.1 ship the engine inside core, behind a hidden `dual_code_graphql_api` feature flag (`wp option update woocommerce_feature_dual_code_graphql_api_enabled yes` to enable it). The plugin stays dormant on those versions and the flag remains the switch there.
diff --git a/docs/apis/dual-api/architecture.md b/docs/apis/dual-api/architecture.md
deleted file mode 100644
index 1a606ad7983..00000000000
--- a/docs/apis/dual-api/architecture.md
+++ /dev/null
@@ -1,85 +0,0 @@
----
-post_title: 'Dual API architecture'
-sidebar_label: 'Architecture'
-sidebar_position: 1
----
-
-# Dual API architecture
-
-This document explains how the pieces fit together. For how to actually write classes, see [Extending the code API](./extending-the-code-api.md).
-
-## Two halves, one source
-
-The dual API has two halves:
-
-- **The code API**: plain PHP classes under `src/Api/`. They are GraphQL-agnostic: they import nothing from any GraphQL library and work as a standalone, in-process PHP API. This is the **authoritative, manually maintained source**.
-- **The autogenerated GraphQL layer**: code under `src/Internal/Api/Autogenerated/` produced by a build script from the code API. It is committed to source control but **never hand-edited**. It powers a GraphQL endpoint (by default `POST|GET /wp-json/wc/graphql`).
-
-The build script reads the code API and (re)generates the GraphQL layer. The relationship is one-directional: you change PHP classes, then regenerate.
-
-```text
-src/Api/ ──(build:api)──▶ src/Internal/Api/Autogenerated/ ──▶ /wp-json/wc/graphql
-(you edit this) (generated, committed, never edited) (GraphQL endpoint)
-```
-
-Because the generated tree is committed to source code, regenerating it after a source change is mandatory; a [staleness check](./building-and-staleness.md) enforces this in GitHub's CI pipeline for pull requests.
-
-## Code-first and the command pattern
-
-The code API is organized around the [**command pattern**](https://en.wikipedia.org/wiki/Command_pattern): each query or mutation is a class with a single `execute()` method (plus an optional `authorize()` method). Output types, input types, enums, interfaces, and scalars are likewise plain classes/enums.
-
-```php
-#[Name( 'product' )]
-#[Description( 'Retrieve a single product by ID.' )]
-#[RequiredCapability( 'read_product' )]
-class GetProduct {
- #[ReturnType( Product::class )]
- public function execute( int $id ): ?object {
- // ...
- }
-}
-```
-
-The build script infers as much as it can from code structure and uses [**PHP 8 attributes**](https://www.php.net/manual/en/language.attributes.php) only where structure is not enough.
-
-## Convention over configuration
-
-Two conventions drive most behavior:
-
-- **Directory placement determines role.** A class in `Queries/` becomes a GraphQL query; one in `Types/` becomes an output type; one in `Enums/` becomes an enum; and so on. Arbitrary nested subdirectories are allowed for organization (e.g. `Queries/Coupons/GetCoupon.php`) - nesting does not change the role. See [Recognized directories](./reference/directories.md).
-- **Names are derived, then overridable.** GraphQL type names default to the PHP class name; query/mutation names to its camelCase form; fields to property names as-is; enum values from PascalCase to `SCREAMING_SNAKE_CASE`. Any of these can be overridden with `#[Name( '...' )]`.
-
-Attributes fill the gaps that conventions cannot: descriptions, authorization, type shaping (arrays, connections, custom scalars), deprecation, and metadata. See the [Attributes reference](./reference/attributes.md).
-
-## The GraphQL engine is an implementation detail
-
-The GraphQL endpoint is currently powered by the [webonyx/graphql-php](https://github.com/webonyx/graphql-php) package, vendored and re-namespaced to `Automattic\WooCommerce\Vendor\GraphQL\*` to avoid version conflicts with other plugins.
-
-This is deliberately hidden from code-API authors. The autogenerated code never references `Vendor\GraphQL\*` directly: it references only a thin, WooCommerce-owned **schema surface** under `Api\Infrastructure\Schema\*`. That surface is the single point of contact with the engine, so the engine could be replaced in the future without breaking already-committed generated code in plugins. As a code-API author you never see GraphQL types at all; as an infrastructure maintainer, see [Extending the infrastructure](./extending-the-infrastructure.md).
-
-## Where things live
-
-| Path | Contents | Edit? |
-| --- | --- | --- |
-| `src/Api/` | The code API: attributes, queries, mutations, types, input types, enums, interfaces, scalars, pagination, utils | Yes, this is the source |
-| `src/Api/Infrastructure/` | Public, engine-decoupled runtime surface and convention classes (`Principal`, `ClassResolver`, `GraphQLControllerBase`, the `Schema\*` wrappers, ...) | Rarely; infrastructure only |
-| `src/Internal/Api/Autogenerated/` | Generated GraphQL resolvers and type definitions | No, regenerate instead |
-| `src/Internal/Api/` | Internal runtime not referenced by external code (`QueryCache`, `Settings`, endpoint registrar, query rules) | Rarely; core only |
-| `bin/api-builder/` | The build tooling (`ApiBuilder`, `build-api.php`, staleness checker, templates). Not shipped in release builds | Rarely; infrastructure only |
-
-The generated tree mirrors the role directories: `Autogenerated/GraphQLQueries/`, `GraphQLMutations/`, and `GraphQLTypes/{Output,Input,Enums,Interfaces,Scalars,Pagination}/`, plus a `RootQueryType`, `RootMutationType`, and `TypeRegistry`.
-
-## Request lifecycle (summarized)
-
-When a GraphQL request hits the endpoint, the controller (a generated subclass of `GraphQLControllerBase`):
-
-1. Resolves a **principal** for the request (who is calling) via the configured `PrincipalResolver`.
-2. Parses and validates the query (depth and complexity limits; optional caching of the parsed AST).
-3. Runs the resolvers, which look up the corresponding command class through the `ClassResolver`, check authorization, and call `execute()`.
-4. Formats the result (or errors) and picks an HTTP status code (optionally via a plugin-supplied `HttpStatusResolver`).
-
-Each of these steps is a documented extension point; see [Authentication and authorization](./authentication-and-authorization.md), [Settings and caching](./caching-and-settings.md), and [Infrastructure classes](./reference/infrastructure-classes.md).
-
-## Reusable by plugins
-
-Everything above applies to a plugin that wants its own dual API. A plugin defines its own `src/Api/` tree, runs the same builder against it, commits the generated output to its own repo, and registers a dedicated GraphQL endpoint. It reuses WooCommerce's infrastructure and can supply its own convention classes (authentication, class resolution, status codes) and attributes where it needs to diverge from the defaults. See [Creating a dual API in a plugin](./creating-a-dual-api-in-a-plugin.md).
diff --git a/docs/apis/dual-api/authentication-and-authorization.md b/docs/apis/dual-api/authentication-and-authorization.md
deleted file mode 100644
index 9935926320a..00000000000
--- a/docs/apis/dual-api/authentication-and-authorization.md
+++ /dev/null
@@ -1,141 +0,0 @@
----
-post_title: 'Authentication and authorization'
-sidebar_label: 'Authentication/Authorization'
-sidebar_position: 4
----
-
-# Authentication and authorization
-
-Authentication and authorization in the dual API revolve around a [**security principal**](https://en.wikipedia.org/wiki/Principal_(computer_security)): a per-request object representing who is calling. Authentication produces the principal; authorization decides what that principal may do, expressed through **attributes**.
-
-## The principal
-
-Each request resolves to exactly one principal, produced once by a `PrincipalResolver`. The default core resolver wraps the current WordPress user:
-
-```php
-final class PrincipalResolver {
- public function resolve_principal(): Principal {
- return new Principal( wp_get_current_user() );
- }
-}
-```
-
-The default `Principal` carries the `WP_User` and exposes:
-
-- `is_authenticated(): bool`: `true` when `user->ID > 0`. Anonymous requests are **not** signalled by `null`; they're a real principal whose user has ID 0.
-- `can_introspect(): bool`: defaults to true only when the user has the `manage_woocommerce` capability.
-- `can_use_debug_mode(): bool`: defaults to true only when the user has the `manage_options` capability.
-
-Plugins authenticating against something else (app token, signed webhook, ...) ship their own `PrincipalResolver` and principal class. The resolver's **return type declares the plugin's principal type**, which ApiBuilder uses to type-check `authorize()`/`$_principal` signatures at build time. A resolver may take an optional `\WP_REST_Request $request` parameter, or none. To reject bad credentials, throw `UnauthorizedException` or `InvalidTokenException` from the resolver. See [Creating a dual API in a plugin](./creating-a-dual-api-in-a-plugin.md) and [Infrastructure classes](./reference/infrastructure-classes.md).
-
-## Authorization attributes
-
-Authorization is declarative. Core ships two attributes:
-
-- `#[PublicAccess]`: no authentication required (`authorize()` always returns `true`).
-- `#[RequiredCapability( 'capability-name' )]`: requires the principal to hold a WordPress capability. Repeatable; multiple capabilities are ANDed (so all the capabilities are required in the user for authorization to succeed).
-
-```php
-#[RequiredCapability( 'read_private_shop_coupons' )]
-class ListCoupons { /* ... */ }
-```
-
-An attribute is recognized as an authorization attribute by **convention**: it declares a public `authorize()` method returning `bool`. The first non-underscore parameter receives the principal:
-
-```php
-public function authorize( MyPrincipal $principal ): bool { /* ... */ }
-// or, for unconditional access:
-public function authorize(): bool { return true; }
-```
-
-Plugins define their own authorization attributes (e.g. `#[RequiresScope( 'events:read' )]`) the same way, see the [Attributes reference](./reference/attributes.md). This is the recommended approach; it keeps authorization separate from business logic.
-
-### The `authorize()` method on commands
-
-For logic that doesn't fit an attribute, a query/mutation class can declare its own `authorize()` method. Compose it with the attribute decision via the `bool $_preauthorized` parameter (which will receive `true` if the attribute gates already grant):
-
-```php
-public function authorize( int $id, bool $_preauthorized, MyPrincipal $_principal ): bool {
- return $_preauthorized || $_principal->owns( $id );
-}
-```
-
-## Granular (type- and field-level) authorization
-
-Authorization attributes apply at four levels:
-
-| Target | Effect |
-| --- | --- |
-| **Query / mutation** (class) | Gates the whole operation. |
-| **Output type** (class) | AND-composed into every field gate of that type (including via a trait the type uses). |
-| **Output field** (property) | Gates that field; re-evaluated per item when the field is a list. |
-| **Input field** (property) | Gates the field, but only when it was actually provided in the request. |
-
-`#[PublicAccess]` on a property is a no-op (it always grants) and produces a build warning.
-
-`authorize()` methods can opt into three more context parameters, supplied per call site, detected by name, in any order:
-
-- `array $_metadata`: `#[Metadata]` entries visible at the call site, in up to three slices: `['query']` (originating operation), `['type']` (enclosing type), `['field']` (the gated field).
-- `array $_args`: the GraphQL arguments at the call site.
-- `mixed $_parent`: the enclosing object being resolved (for an output-field gate, the parent object; lets you implement owner-or-scope checks).
-
-```php
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY )]
-final class OwnerOrScope {
- public function __construct( public readonly string $scope ) {}
-
- public function authorize( EventsPrincipal $principal, mixed $_parent ): bool {
- return $principal->has_scope( $this->scope )
- || ( is_object( $_parent ) && $_parent->organizer_login === $principal->user_login );
- }
-}
-```
-
-### Deny shape and HTTP status
-
-When a gate denies:
-
-- **Operation-level** denies produce the bare authorization error.
-- **Field-level** denies attach `extensions.subject = { type, field, attribute }` alongside the preserved `extensions.code`.
-
-The error code and HTTP status depend on whether the principal is authenticated:
-
-- **Anonymous** principal (`is_authenticated()` returns `false`) → `UNAUTHORIZED` / **401** (authenticating might help).
-- **Authenticated** principal, or one that doesn't expose `is_authenticated()` → `FORBIDDEN` / **403** (authenticating won't help).
-
-Credential problems surfaced by the resolver use `UNAUTHORIZED` (401) or `INVALID_TOKEN` (401). See [Exceptions](./reference/exceptions.md).
-
-## Introspection, debug mode, and metadata gating
-
-Three sensitive surfaces are gated independently, each by a combination of a principal method, a filter, and a fail-closed default:
-
-| Surface | Principal method | Filter | Default if method absent |
-| --- | --- | --- | --- |
-| Native introspection (`__schema`, `__type`) | `can_introspect()` | `woocommerce_graphql_can_introspect` | deny |
-| Debug mode (also requires `_debug=1`) | `can_use_debug_mode()` | `woocommerce_graphql_can_use_debug_mode` | deny |
-| `_apiMetadata` discovery | `can_query_metadata()`, else falls back to `can_introspect()` | `woocommerce_graphql_can_query_metadata` | deny |
-
-All three gates **fail closed**:
-
-- A `null`/unresolved principal denies.
-- The principal method's return is checked with `=== true` (a truthy non-bool denies).
-- A throw from the method or filter is caught and treated as a deny.
-- Filters must return strictly `true` to grant; loose values like `1` or `'yes'` deny.
-
-The filters receive `( bool $decision, ?object $principal, \WP_REST_Request $request )`. They are **not** invoked when principal resolution itself failed. They are also **site-wide**: a callback affects every dual-API endpoint on the site (core and plugins), so branch on the `$request` route if it should apply to only one; see [Scope: what applies where](./caching-and-settings.md#scope-what-applies-where). The core `Principal` declares `can_introspect()` (gated on `manage_woocommerce`), which also governs `_apiMetadata` since it has no `can_query_metadata()` - so admin access to both works out of the box, and other principals are denied unless they opt in.
-
-Example override:
-
-```php
-add_filter(
- 'woocommerce_graphql_can_introspect',
- fn( bool $can, $principal, \WP_REST_Request $request ): bool =>
- $can || 'true' === $request->get_param( 'x-allow-introspection' ),
- 10,
- 3
-);
-```
-
-## Pre-authorization for code-API callers
-
-Code that calls the code API directly (not through GraphQL) can ask whether the attribute gates would grant access for a principal, without executing the command, via `ResolverHelpers::compute_preauthorized( string $command_fqcn, object $principal ): bool`.
diff --git a/docs/apis/dual-api/building-and-staleness.md b/docs/apis/dual-api/building-and-staleness.md
deleted file mode 100644
index c6812def25a..00000000000
--- a/docs/apis/dual-api/building-and-staleness.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-post_title: 'Building and staleness checks'
-sidebar_label: 'Building'
-sidebar_position: 7
----
-
-# Building and staleness checks
-
-The GraphQL layer is generated from the code API by a build script and **committed to source control**. This document covers the commands and the check that keeps the committed output in sync with its source.
-
-## Regenerating
-
-From `plugins/woocommerce/`:
-
-```bash
-# Regenerate core's GraphQL layer from src/Api/
-pnpm --filter=@woocommerce/plugin-woocommerce build:api
-
-# Regenerate the test fixture's tree (the DummyApi used by the test suite)
-pnpm --filter=@woocommerce/plugin-woocommerce build:api:test
-
-# Check whether the committed output is stale (used by CI)
-pnpm --filter=@woocommerce/plugin-woocommerce build:api:check
-```
-
-`build:api` runs `php bin/api-builder/build-api.php`. It **wipes and regenerates** the output directory, formats the result with `phpcbf`, refreshes the Composer autoloader, and writes the staleness-tracking files. The build tooling under `bin/api-builder/` is excluded from release builds.
-
-After regenerating, commit the `src/Api/` change and the regenerated `src/Internal/Api/Autogenerated/` tree **together**.
-
-## `build-api.php` flags
-
-`build-api.php` with no flags produces core's output. The four path/namespace flags are **all-or-nothing** - provide all four or none:
-
-| Flag | Meaning |
-| --- | --- |
-| `--api-dir=PATH` | Directory of code-API source classes to scan. |
-| `--autogen-dir=PATH` | Output directory (**wiped each run**). |
-| `--api-namespace=NS` | PSR-4 namespace mapping to `--api-dir`. |
-| `--autogen-namespace=NS` | PSR-4 namespace mapping to `--autogen-dir`. |
-| `--composer-working-dir=DIR` | Where to run `composer dump-autoload`. |
-| `--phpcbf-path=PATH` | Path to the `phpcbf` binary used for formatting. |
-| `--no-linter` | Skip the `phpcbf` formatting pass. |
-
-For fast local iteration, pass `--no-linter`: the `phpcbf` pass is the slowest step in the build, and it only affects whitespace in the generated PHP — the code is functionally identical with or without it. Run a full build (without the flag) before committing so the formatted output is what lands in source control. `ApiBuilder::run_for_plugin()` also honours `--no-linter` from `argv`, so the same shortcut works for plugin builds: `WC_PATH=… php bin/build-api.php --no-linter`.
-
-Plugins generally don't call the other flags directly; they use `ApiBuilder::run_for_plugin()` (see [Creating a dual API in a plugin](./creating-a-dual-api-in-a-plugin.md)).
-
-## The staleness check
-
-`build:api:check` (`php bin/api-builder/check-api-staleness.php`) fails when the committed generated tree doesn't match the current source.
-
-The check is **content-based**, not timestamp-based: `build:api` writes a SHA-256 hash of every `.php` file under the source dir (each file hashed as `relative_path \0 contents \0`, files sorted by path) into `api_source_hash.txt` in the output directory. `StalenessChecker::is_stale()` recomputes that hash and compares. Because it ignores mtimes and filesystem iteration order, it behaves identically on fresh clones, in CI, and during active development. (`api_generation_date.txt` is also written, for human reference only.)
-
-## CI enforcement
-
-The `.github/workflows/api-staleness.yml` workflow (**GraphQL API Staleness Check**) runs `build:api:check` on PRs and pushes that touch:
-
-- `plugins/woocommerce/src/Api/**`
-- `plugins/woocommerce/src/Internal/Api/Autogenerated/**`
-- `plugins/woocommerce/bin/api-builder/**`
-- the workflow file itself
-
-If the source was changed without regenerating, the job fails with `Generated GraphQL API code is out of date.` Regenerate, commit, and push to clear it. Plugins that maintain their own dual API should add an equivalent check to their own CI.
diff --git a/docs/apis/dual-api/caching-and-settings.md b/docs/apis/dual-api/caching-and-settings.md
deleted file mode 100644
index cbe663f7b70..00000000000
--- a/docs/apis/dual-api/caching-and-settings.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-post_title: 'Settings and caching'
-sidebar_label: 'Settings and caching'
-sidebar_position: 6
----
-
-# Settings and caching
-
-WooCommerce core's GraphQL endpoint is configured under **WooCommerce → Settings → Advanced → GraphQL**. The section appears only when the `dual_code_graphql_api` feature flag is on.
-
-These settings are **site-wide, not per-endpoint**: every setting below except **Endpoint URL** applies to *every* dual-API endpoint on the site, including those registered by plugins. See [Scope: what applies where](#scope-what-applies-where).
-
-## Settings
-
-| Setting | Option name (`Main::` constant) | Type | Default | Effect |
-| --- | --- | --- | --- | --- |
-| Endpoint URL | `woocommerce_graphql_endpoint_url` (`OPTION_ENDPOINT_URL`) | text | `wc/graphql` | **Core's `/wc/graphql` only.** Path under `/wp-json/`. Must be at least two segments (`namespace/route`); validated and normalized on save. Plugins set their own route when they register an endpoint, so this setting does not affect them. |
-| Enable GET endpoint | `woocommerce_graphql_get_endpoint_enabled` (`OPTION_GET_ENDPOINT_ENABLED`) | checkbox | `yes` | When off, the endpoint accepts POST only; GET returns 404. Mutations are always rejected over GET. |
-| Maximum query depth | `woocommerce_graphql_max_query_depth` (`OPTION_MAX_QUERY_DEPTH`) | number | `15` | Rejects queries nested deeper than this during validation. Falls back to default when unset or non-positive. |
-| Maximum query complexity | `woocommerce_graphql_max_query_complexity` (`OPTION_MAX_QUERY_COMPLEXITY`) | number | `1000` | Rejects queries whose computed complexity score exceeds this. Connection fields multiply child cost by page size. |
-| Parsed query cache TTL | `woocommerce_graphql_query_cache_ttl` (`OPTION_QUERY_CACHE_TTL`) | number | `86400` | Seconds before cached parsed queries expire (object cache and APQ paths). |
-| Enable OPcache-based caching | `woocommerce_graphql_opcache_enabled` (`OPTION_OPCACHE_ENABLED`) | checkbox | `yes` | Cache parsed ASTs as PHP files served from OPcache shared memory. |
-| Enable ObjectCache-based caching | `woocommerce_graphql_object_cache_enabled` (`OPTION_OBJECT_CACHE_ENABLED`) | checkbox | `yes` | Cache parsed ASTs in the WP object cache. |
-| Enable APQ caching | `woocommerce_graphql_apq_enabled` (`OPTION_APQ_ENABLED`) | checkbox | `yes` | Support the Apollo Automatic Persisted Queries protocol (`persistedQuery` extension). When off, hash-only requests are rejected. |
-
-The depth and complexity metrics are observable on a request by appending `?_debug=1` (when the principal may use debug mode); the response carries `extensions.debug.depth` and `extensions.debug.complexity`.
-
-## Scope: what applies where
-
-The dual API has one set of switches and filters shared by every endpoint on the site, there is no per-plugin configuration surface. Concretely:
-
-- **The `dual_code_graphql_api` feature flag gates every dual-API endpoint.** When it's off, neither core's `/wc/graphql` nor any plugin endpoint is registered (`Main::register_graphql_endpoint()` is a no-op). PHP 8.1+ is required the same way.
-- **Every setting except Endpoint URL applies to all endpoints.** The GET toggle, max depth, max complexity, the three caching toggles, and the cache TTL are read from the shared infrastructure, so a plugin endpoint honours them exactly as core's does (for example, plugin endpoints reject GET when the GET toggle is off). **Endpoint URL is the exception**: it only configures core's `/wc/graphql`; a plugin chooses its own route at registration.
-- **The filters below are global.** A callback added to any of them affects *every* dual-API endpoint on the site, core and plugins alike. Each filter receives the `\WP_REST_Request`, so a callback that should apply to only one endpoint must branch on the request's route itself.
-
-## Query caching
-
-Parsing a GraphQL query into an AST is the expensive, repeatable step, so the framework caches parsed ASTs. On each request the resolution chain is:
-
-1. **OPcache file backend**: when its toggle is on, the OPcache extension is loaded, and the cache directory is writable. Parsed ASTs are written as `return [...];` PHP files under `wp-content/uploads/wc-graphql-cache/v<engine-version>/`; OPcache serves them as compiled bytecode (no string parse, no `unserialize`, no remote cache call).
-2. **WP object cache**: otherwise, when its toggle is on.
-3. **No cache**: parse on every request.
-
-Notes:
-
-- The cache key/version is tied to the query string and the parser version, so there's no correctness TTL concern on the file backend; the configurable TTL applies to the object-cache and APQ paths.
-- OPcache writes are atomic (temp file + `rename()`), drop a deny-all `.htaccess`, and pre-warm the bytecode. Expired files are cleaned up via a scheduled `woocommerce_graphql_opcache_cleanup` action.
-- APQ always uses the object cache for hash-only lookups, regardless of the standard-query toggles, preserving persisted-query semantics.
-
-## Relevant filters
-
-| Filter | Signature | Purpose |
-| --- | --- | --- |
-| `woocommerce_graphql_opcache_cache_dir` | `( string $dir )` | Override the OPcache file directory (default `{uploads}/wc-graphql-cache/v<n>`). Empty strings and stream wrappers are rejected. |
-| `woocommerce_graphql_can_introspect` | `( bool, ?object $principal, \WP_REST_Request )` | Gate native introspection. See [Authentication and authorization](./authentication-and-authorization.md). |
-| `woocommerce_graphql_can_use_debug_mode` | `( bool, ?object $principal, \WP_REST_Request )` | Gate debug mode. |
-| `woocommerce_graphql_can_query_metadata` | `( bool, ?object $principal, \WP_REST_Request )` | Gate `_apiMetadata`. See [Metadata](./metadata.md). |
-
-## Customizing the response HTTP status
-
-A plugin can override the HTTP status of any response (for example, always return 200) by shipping an `HttpStatusResolver` convention class. Core ships none, so its per-error-code mapping is the default. See [Creating a dual API in a plugin](./creating-a-dual-api-in-a-plugin.md) and [Infrastructure classes](./reference/infrastructure-classes.md).
diff --git a/docs/apis/dual-api/creating-a-dual-api-in-a-plugin.md b/docs/apis/dual-api/creating-a-dual-api-in-a-plugin.md
deleted file mode 100644
index da544256523..00000000000
--- a/docs/apis/dual-api/creating-a-dual-api-in-a-plugin.md
+++ /dev/null
@@ -1,113 +0,0 @@
----
-post_title: 'Creating a dual API in a plugin'
-sidebar_label: 'Dual API in a plugin'
-sidebar_position: 8
----
-
-# 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`](https://github.com/woocommerce/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](./reference/directories.md) 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](./extending-the-code-api.md).
-
-## 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](./building-and-staleness.md), 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](./caching-and-settings.md).
-
-## 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.
-
-| Class | Default | Ship your own to… |
-| --- | --- | --- |
-| `ClassResolver` | `wc_get_container()->get()` | Instantiate commands through your own DI container. |
-| `PrincipalResolver` | wraps `wp_get_current_user()` | Authenticate against something other than WP users. Its return type declares your principal type. |
-| `Principal` | wraps `WP_User` | Carry your own identity/permission data. Add `is_authenticated()`, and `can_introspect()`/`can_query_metadata()`/`can_use_debug_mode()` to opt into those surfaces. |
-| `HttpStatusResolver` | none (per-error-code map) | Override response HTTP status, e.g. always return 200. |
-
-See [Infrastructure classes](./reference/infrastructure-classes.md) 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](./reference/attributes.md). 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](./reference/exceptions.md).
-
-## 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](./architecture.md) and [Extending the infrastructure](./extending-the-infrastructure.md).
diff --git a/docs/apis/dual-api/extending-the-code-api.md b/docs/apis/dual-api/extending-the-code-api.md
deleted file mode 100644
index 464cc685b50..00000000000
--- a/docs/apis/dual-api/extending-the-code-api.md
+++ /dev/null
@@ -1,143 +0,0 @@
----
-post_title: 'Extending the code API'
-sidebar_label: 'Extending the code API'
-sidebar_position: 2
----
-
-# 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](./creating-a-dual-api-in-a-plugin.md) for the plugin-specific bootstrap).
-
-> Reminder: core's `coupons`/`products` API is a proof of concept and may change. Use it as a pattern, not a stable contract.
-
-## The workflow
-
-1. Add or edit classes under `src/Api/`.
-2. Regenerate the GraphQL layer: `pnpm --filter=@woocommerce/plugin-woocommerce build:api`.
-3. Run the tests and the [staleness check](./building-and-staleness.md).
-4. 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]`.
-
-```php
-#[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 $id` is 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](#enums-interfaces-scalars) below), and a trait can't be used as a return type hint - declare it with `#[ReturnType( SomeInterface::class )]` and return `object`.
-- **Errors:** throw a plain `\InvalidArgumentException` for malformed input (will be mapped to `INVALID_ARGUMENT` / 400), or one of the [exception classes](./reference/exceptions.md) 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.
-
-```php
-#[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 an `array` property.
-- `#[ScalarType( DateTime::class )]`: render a property through a custom scalar. The property is typically a `string` holding the scalar's raw form (e.g. an ISO date), but it isn't required to be: any value the scalar's `serialize()` accepts works; the property's nullability still controls the field's nullability.
-- `#[ConnectionOf( SomeType::class )]` on a `Connection`-typed property: a nested paginated connection field (see [Relay-style pagination](./pagination.md)).
-- `#[Deprecated( 'reason' )]`: mark the field as deprecated (will be visible as such in [GraphQL introspection](https://graphql.org/learn/introspection/)).
-- `#[Ignore]`: exclude the property from the schema.
-- `#[Parameter( ... )]` / `#[ParameterDescription( ... )]`: give a field computed arguments (e.g. a `formatted` flag on a price field).
-
-See the [Attributes reference](./reference/attributes.md) 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:
-
-```php
-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 to `SCREAMING_SNAKE_CASE` (e.g. `FixedCart` → `FIXED_CART`); override with `#[Name]`. Add `#[Description]` to the enum and each case. A common pattern is an `Other` case plus a `raw_*` field on the type, so plugin-added values don't break the enum.
-- **Interfaces** (`Interfaces/`) are PHP **traits** marked with `#[Name]`/`#[Description]`. A type that `use`s the trait implements the interface. Traits can compose other traits (e.g. `Product` uses `ObjectWithId`).
-- **Scalars** (`Scalars/`) are classes with static `serialize( mixed $value ): string` (PHP → transport) and `parse( string $value ): mixed` (client → PHP, throwing `\InvalidArgumentException` on bad input). Apply one to a field with `#[ScalarType]`.
-
-### Why interfaces are PHP traits
-
-GraphQL interfaces are modeled as PHP **traits** rather than PHP `interface`s 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 `use`s 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](#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](https://graphql.org/learn/pagination/) with [Relay-style cursor connections](https://relay.dev/graphql/connections.htm): return a `Connection` and declare the node type with `#[ConnectionOf( <NodeType>::class )]`, taking a `PaginationParams` argument (which `#[Unroll]`s into `first` / `last` / `after` / `before`).
-
-```php
-#[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](./pagination.md)**.
-
-## 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`: in `authorize()`, whether the attribute-based gates already grant access (lets you compose custom logic on top).
-- `array $_metadata`, `array $_args`, `mixed $_parent`: context for `authorize()` in granular (type/field) authorization.
-
-See [Recognized methods and parameters](./reference/recognized-methods-and-parameters.md) for the full contract, and [Authentication and authorization](./authentication-and-authorization.md) for how authorization is wired.
-
-## After you change anything
-
-Regenerate and commit the generated tree. The CI [staleness check](./building-and-staleness.md) fails any PR whose `src/Api/` source doesn't match its committed `Autogenerated/` output.
diff --git a/docs/apis/dual-api/extending-the-infrastructure.md b/docs/apis/dual-api/extending-the-infrastructure.md
deleted file mode 100644
index d2e4a5a153d..00000000000
--- a/docs/apis/dual-api/extending-the-infrastructure.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-post_title: 'Extending the infrastructure'
-sidebar_label: 'Extending the infrastructure'
-sidebar_position: 9
----
-
-# 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` 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.
diff --git a/docs/apis/dual-api/metadata.md b/docs/apis/dual-api/metadata.md
deleted file mode 100644
index c47d984171c..00000000000
--- a/docs/apis/dual-api/metadata.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-post_title: 'Metadata and discovery'
-sidebar_label: 'Metadata'
-sidebar_position: 5
----
-
-# 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 infrastructure 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`.
-
-```php
-#[Metadata( 'owner', 'payments-team' )]
-#[Metadata( 'beta', true )]
-class SomeType { /* ... */ }
-```
-
-Core ships two convenience subclasses:
-
-- `#[Internal]` — `name = 'internal'`, `value = true`. For WooCommerce-core-only elements.
-- `#[Experimental]` — `name = 'experimental'`, `value = true`.
-
-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()`:
-
-- `#[Internal]` prefixes the description with `[Internal] ` and supplies a default body when none exists.
-- `#[Experimental]` does the same with `[Experimental] `.
-
-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 `__( ..., 'woocommerce' )` translation pipeline. 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](./reference/attributes.md).
-
-## Discovery via GraphQL: `_apiMetadata`
-
-Every generated schema gains a root field:
-
-```graphql
-_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](./authentication-and-authorization.md). 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()`.
-
-## Discovery via PHP: `SchemaHandle`
-
-For in-process inspection, `GraphQLControllerBase::get_schema()` returns an opaque `SchemaHandle` (`Automattic\WooCommerce\Api\Utils\SchemaHandle`) with:
-
-- `get_all_metadata(): array`: every metadata row in the schema.
-- `find_metadata( ?string $name, ?string $type, ?string $field ): array`: the same filter-narrows semantics as the GraphQL field.
-
-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.
diff --git a/docs/apis/dual-api/pagination.md b/docs/apis/dual-api/pagination.md
deleted file mode 100644
index 16811b5cf25..00000000000
--- a/docs/apis/dual-api/pagination.md
+++ /dev/null
@@ -1,119 +0,0 @@
----
-post_title: 'Relay-style pagination'
-sidebar_label: 'Pagination'
-sidebar_position: 3
----
-
-# Relay-style pagination
-
-List queries in the dual API paginate with **cursor-based connections** following the [Relay Cursor Connections specification](https://relay.dev/graphql/connections.htm). You write a command that returns a `Connection`; the builder generates the matching GraphQL `Connection`, `Edge`, and shared `PageInfo` types. The building blocks live in `Automattic\WooCommerce\Api\Pagination` and are reused by core and plugins alike.
-
-## The connection shape
-
-For a node type `Coupon`, a `#[ConnectionOf( Coupon::class )]` query produces this GraphQL shape:
-
-```graphql
-type CouponConnection {
- edges: [CouponEdge!]! # each item paired with its cursor
- nodes: [Coupon!]! # the items alone, a convenience shortcut
- page_info: PageInfo!
- total_count: Int! # total matches before the page window
-}
-
-type CouponEdge {
- cursor: String!
- node: Coupon!
-}
-
-type PageInfo {
- has_next_page: Boolean!
- has_previous_page: Boolean!
- start_cursor: String
- end_cursor: String
-}
-```
-
-`edges` and `nodes` carry the same items; `edges` adds the per-item `cursor`, while `nodes` is there for clients that just want the data. `PageInfo` is a single shared type across every connection.
-
-## Writing a paginated query
-
-Place the query under `Queries/`, return a `Connection`, and annotate `execute()` with `#[ConnectionOf( <NodeType>::class )]`. Take an argument of type `PaginationParams` - this type carries `#[Unroll]`, so its properties expand into individual GraphQL arguments rather than a nested input object:
-
-```php
-#[Name( 'coupons' )]
-#[Description( 'List coupons with cursor-based pagination.' )]
-#[RequiredCapability( 'read_private_shop_coupons' )]
-class ListCoupons {
- #[ConnectionOf( Coupon::class )]
- public function execute( PaginationParams $pagination, ?CouponStatus $status = null ): Connection {
- // 1. query your data store, fetching one extra row to detect a next page
- // 2. build an Edge per item (cursor + node)
- // 3. populate a PageInfo and total_count
- // 4. return the Connection
- }
-}
-```
-
-The resulting field accepts the four standard arguments plus any others you declare (like `status` above):
-
-```graphql
-coupons(first: Int, last: Int, after: String, before: String, status: CouponStatus) { ... }
-```
-
-## The pagination arguments
-
-`PaginationParams` defines the forward/backward window:
-
-| Argument | Meaning |
-| --- | --- |
-| `first` | Return the first N items (forward pagination). |
-| `after` | Return items after this cursor. |
-| `last` | Return the last N items (backward pagination). |
-| `before` | Return items before this cursor. |
-
-Bounds are enforced: `first`/`last` must be between `0` and `PaginationParams::MAX_PAGE_SIZE`; a negative or over-cap value throws `INVALID_ARGUMENT` (HTTP 400). When neither `first` nor `last` is given, `PaginationParams::get_default_page_size()` applies. The same bounds are enforced on nested connection fields via `PaginationParams::validate_args()`, so a deeply nested `first: 1000` can't slip past the cap.
-
-These maximum and default page sizes are currently hardcoded to 100, but may become configurable in future versions of WooCommerce.
-
-## Cursors
-
-Cursors are **opaque strings** to the client, never construct or parse them on the client side. Beyond that opacity, the engine mandates nothing about their format: any stable, encodable key works. The current core proof-of-concept happens to encode the node's numeric id as base64 (`base64_encode( (string) $id )`) and decode it with `IdCursorFilter::decode_id_cursor()`, which validates the input and throws `INVALID_ARGUMENT` (400) on a malformed cursor rather than silently returning unfiltered results. That scheme is a choice of the PoC code, not a requirement; your own connections are free to use a different encoding - just keep cursors opaque and validate them on decode.
-
-`IdCursorFilter` (in the `Api\Pagination` namespace) is a helper the PoC uses to window WordPress post queries on the `ID` column, via a lazy `posts_where` filter and two query vars:
-
-- `IdCursorFilter::AFTER_ID` (`wc_api_after_id`) → `AND ID > X`
-- `IdCursorFilter::BEFORE_ID` (`wc_api_before_id`) → `AND ID < X`
-
-Set whichever you need on your `WP_Query` args and call `IdCursorFilter::ensure_registered()` once before running the query. None of this is mandated by the engine: a plugin paginating its own post-backed data may find it useful to reuse `IdCursorFilter` (or follow the same `ID`-cursor pattern), but it's specific to `WP_Query` sources, and a connection over any other data store won't touch it.
-
-## PageInfo semantics
-
-- `start_cursor` / `end_cursor` are the cursors of the first and last edges in the returned page (or `null` for an empty page).
-- `has_next_page` / `has_previous_page` follow the Relay rules. In **forward** pagination (`first`), `has_next_page` is true when more items exist after the window - the common "fetch N+1 and check" trick. In **backward** pagination (`last`), the roles mirror. The framework computes these for you when it slices; if you pre-slice, you set them yourself.
-
-## Building the Connection: two paths
-
-`Connection` supports both a performant pre-paginated path and a slice-it-for-me path, and it guards against being sliced twice (so it's safe whether or not the generated resolver also calls `slice()`):
-
-- **`Connection::pre_sliced( array $edges, PageInfo $page_info, int $total_count )`**: use when your data store already applied the limits (the recommended path for real databases: push `first`/`after` into the SQL query). The returned connection is marked sliced, so the framework leaves it untouched.
-- **`$connection->slice( array $args )`**: build a `Connection` over a larger (or full) result set and let it apply the Relay algorithm: narrow by `after`, then `before`, then take `first` or `last`. It recomputes `PageInfo` and returns a new, sliced connection. Convenient for in-memory or small result sets.
-
-## Nested connections
-
-A `Connection`-typed **property** on an output type, annotated with `#[ConnectionOf]`, becomes a paginated field on that type; for example `Product.reviews`:
-
-```php
-#[Description( 'Customer reviews for this product.' )]
-#[ConnectionOf( ProductReview::class )]
-public Connection $reviews;
-```
-
-The generated resolver slices the property per the field's own pagination arguments, enforcing the same `MAX_PAGE_SIZE` cap as top-level queries.
-
-## Complexity
-
-Connection fields contribute to a query's computed complexity: a connection's cost multiplies its children's cost by the requested page size. This is what the **Maximum query complexity** limit guards against, see [Settings and caching](./caching-and-settings.md).
-
-## Reusing the building blocks
-
-`Connection`, `Edge`, `PageInfo`, and `PaginationParams` are part of the public `Api\Pagination` surface, so a plugin can return them directly without redefining its own. The [`woocommerce-simple-events`](https://github.com/woocommerce/woocommerce-simple-events) plugin's `eventsConnection` query is a minimal, in-memory working example (it builds edges over the full set and calls `slice()`); core's `ListCoupons` shows the `WP_Query` + `IdCursorFilter` database path.
diff --git a/docs/apis/dual-api/reference/_category_.json b/docs/apis/dual-api/reference/_category_.json
deleted file mode 100644
index 75879876ee2..00000000000
--- a/docs/apis/dual-api/reference/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "label": "Reference",
- "position": 10
-}
diff --git a/docs/apis/dual-api/reference/attributes.md b/docs/apis/dual-api/reference/attributes.md
deleted file mode 100644
index 9e63acd4d66..00000000000
--- a/docs/apis/dual-api/reference/attributes.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-post_title: 'Reference: attributes'
-sidebar_label: 'Attributes'
-sidebar_position: 2
----
-
-# Reference: attributes
-
-PHP 8 attributes supply the metadata the builder can't infer from code structure. All built-in attributes live in `Automattic\WooCommerce\Api\Attributes`. Plugins define their own under their `Api\Attributes\` namespace, following the [conventions](#conventions-for-custom-attributes) below; those conventions also apply when adding attributes to core.
-
-## Naming and description
-
-| Attribute | Constructor | Targets | Purpose |
-| --- | --- | --- | --- |
-| `Name` | `( string $name )` | all | Override the derived GraphQL name of a type, field, query/mutation, or enum value. |
-| `Description` | `( string $description )` | all | Human-readable description, surfaced in the schema. |
-| `ParameterDescription` | `( string $name, string $description )` | all, repeatable | Describe a single argument by name (e.g. a computed field's `#[Parameter]`). |
-
-## Type shaping
-
-| Attribute | Constructor | Targets | Purpose |
-| --- | --- | --- | --- |
-| `ArrayOf` | `( string $type )` | all | Element type of an `array` property/return: a scalar name (`'int'`, `'string'`, `'float'`, `'bool'`) or a class name. |
-| `ScalarType` | `( string $type )` | all | Render a property through a custom scalar class (e.g. `DateTime::class`). |
-| `ConnectionOf` | `( string $type )` | all | Mark a `Connection` return or property as a connection of the given node type; generates `<Type>Connection`/`<Type>Edge`. |
-| `ReturnType` | `( string $type )` | method | Declare the GraphQL return type when `execute()` returns an interface (PHP can't type-hint a trait). |
-| `Parameter` | see below | all, repeatable | Declare an explicit argument. Used to give an output field computed arguments, or to shape/`unroll` a query argument. |
-| `Unroll` | `()` | class, parameter | Expand a class's public properties into individual flat arguments instead of one input object. |
-
-`Parameter` full signature:
-
-```php
-public function __construct(
- public readonly string $name = '',
- public readonly string $type = '',
- public readonly bool $nullable = false,
- public readonly bool $array = false,
- public readonly mixed $default = null,
- public readonly string $description = '',
- bool $has_default = false,
- public readonly bool $unroll = false,
-)
-```
-
-## Lifecycle
-
-| Attribute | Constructor | Targets | Purpose |
-| --- | --- | --- | --- |
-| `Deprecated` | `( string $reason )` | all | Mark a field or enum value deprecated (shown in introspection). |
-| `Ignore` | `()` | all | Exclude the class or property from the schema entirely. |
-
-## Authorization
-
-| Attribute | Constructor | Targets | Purpose |
-| --- | --- | --- | --- |
-| `PublicAccess` | `()` | class, property | No authentication required. `authorize()` returns `true`. A no-op (and build warning) on a property. |
-| `RequiredCapability` | `( string $capability )` | class, property, repeatable | Require a WordPress capability; `authorize( Principal $principal )` checks `user_can()`. Multiple are ANDed. |
-
-Both can gate queries/mutations (class), output/input types (class), and output/input fields (property). A class-level gate AND-composes into every field gate of the type. See [Authentication and authorization](../authentication-and-authorization.md).
-
-## Metadata
-
-| Attribute | Constructor | Targets | Purpose |
-| --- | --- | --- | --- |
-| `Metadata` | `( string $name, bool\|int\|float\|string\|null $value )` | class, property, parameter, enum case, repeatable | Attach one name/value entry. Base class for custom categories. |
-| `Internal` | `()` | class, property, enum case | `Metadata( 'internal', true )` + `[Internal] ` description prefix. |
-| `Experimental` | `()` | class, property, enum case | `Metadata( 'experimental', true )` + `[Experimental] ` description prefix. |
-| `HiddenFromMetadataQuery` | `()` | class, property, parameter, enum case | Omit the target from `_apiMetadata` discovery (`shows_in_metadata_query()` returns `false`). Does not affect native introspection or runtime gates. |
-
-`Metadata` methods: `get_name()`, `get_value()`, and the overridable `transform_description( string $description ): string` (no-op in the base). Duplicate names on one target are a build error. See [Metadata and discovery](../metadata.md).
-
-## Conventions for custom attributes
-
-The builder recognizes custom attributes by **duck-typed conventions**, not by a base class or interface (except metadata). Declare the PHP `#[Attribute(...)]` targets you want to support.
-
-- **Authorization attribute**: declares a public `authorize(): bool` method. Its first non-underscore parameter receives the principal; the parameter type should be the registered principal type. It may also declare the opt-in context parameters `array $_metadata`, `array $_args`, `mixed $_parent` (see [Recognized methods and parameters](./recognized-methods-and-parameters.md)). To gate fields/arguments as well as operations, include `Attribute::TARGET_PROPERTY` in the `#[Attribute(...)]` declaration.
-- **Metadata attribute**: extends `Metadata` and calls `parent::__construct( $name, $value )`. Discoverable through `_apiMetadata`.
-- **Description-mirroring attribute**: a `Metadata` subclass that overrides `transform_description()`. Transforms chain in source order.
-- **Metadata-query opt-out**: declares `shows_in_metadata_query(): bool` returning `false` (what `#[HiddenFromMetadataQuery]` does).
-
-If you reference an attribute without importing it, PHP resolves it to a non-existent class in the current namespace and silently ignores it; the builder emits a warning naming the FQCN it tried to load, so add the missing `use`.
diff --git a/docs/apis/dual-api/reference/directories.md b/docs/apis/dual-api/reference/directories.md
deleted file mode 100644
index 0400c3a67b9..00000000000
--- a/docs/apis/dual-api/reference/directories.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-post_title: 'Reference: recognized directories'
-sidebar_label: 'Directories'
-sidebar_position: 1
----
-
-# Reference: recognized directories
-
-The builder determines a class' role from the directory it lives in, relative to the code-API root (`src/Api/` for core, `<plugin>/src/Api/` for a plugin). Arbitrary nested subdirectories are allowed for organization and do **not** change the role; e.g. `Queries/Coupons/GetCoupon.php` and `Queries/GetCoupon.php` are both queries.
-
-| Directory | Role | What it holds |
-| --- | --- | --- |
-| `Queries/` | GraphQL query | Command classes with an `execute()` method. Name defaults to camelCase of the class name. |
-| `Mutations/` | GraphQL mutation | Command classes with an `execute()` method. Rejected over GET. |
-| `Types/` | Output type | Plain classes whose public properties become fields. |
-| `InputTypes/` | Input type | Plain classes used as `execute()` arguments; a field is optional when its type is nullable or it has a default. |
-| `Enums/` | Enum type | Backed PHP enums. Case names become `SCREAMING_SNAKE_CASE`. |
-| `Interfaces/` | Interface | PHP **traits** marked `#[Name]`/`#[Description]`; types `use` them to implement. |
-| `Scalars/` | Custom scalar | Classes with static `serialize()` / `parse()`. Applied to fields via `#[ScalarType]`. |
-| `Pagination/` | Pagination support | `Connection`, `Edge`, `PageInfo`, `PaginationParams`, cursor helpers (provided by core; reused, not redefined). |
-| `Attributes/` | Attribute definitions | Custom PHP 8 attributes (authorization, metadata, …). See [Attributes](./attributes.md). |
-| `Infrastructure/` | Convention classes | Optional per-plugin `ClassResolver`, `PrincipalResolver`, principal class, `HttpStatusResolver`. See [Infrastructure classes](./infrastructure-classes.md). |
-| `Utils/` | Helpers | Mappers, repositories, and other plain helpers. Not exposed in the schema. |
-
-Notes:
-
-- Classes the builder shouldn't expose can be excluded with `#[Ignore]` regardless of placement (e.g. a helper that happens to live under a scanned directory).
-- The generated output mirrors these roles under `Internal/Api/Autogenerated/` (`GraphQLQueries/`, `GraphQLMutations/`, `GraphQLTypes/{Output,Input,Enums,Interfaces,Scalars,Pagination}/`), but you never edit that tree; see [Building and staleness checks](../building-and-staleness.md).
-- These conventions are identical for core and for plugins.
diff --git a/docs/apis/dual-api/reference/exceptions.md b/docs/apis/dual-api/reference/exceptions.md
deleted file mode 100644
index c5c49bc7650..00000000000
--- a/docs/apis/dual-api/reference/exceptions.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-post_title: 'Reference: exceptions'
-sidebar_label: 'Exceptions'
-sidebar_position: 5
----
-
-# Reference: exceptions
-
-Throwing an exception from `execute()` or `authorize()` is how the code API surfaces errors. The framework translates each into a GraphQL error with a machine-readable `extensions.code` and a matching HTTP status. All built-in exceptions live in `Automattic\WooCommerce\Api`.
-
-## The base: `ApiException`
-
-```php
-public function __construct(
- string $message,
- private readonly string $error_code = 'INTERNAL_ERROR',
- private readonly array $extensions = array(),
- int $status_code = 500,
- ?\Throwable $previous = null,
-)
-```
-
-It extends `\RuntimeException` and exposes `getErrorCode()`, `getExtensions()`, and `getStatusCode()`. The controller merges your `extensions` with `{ code: <error_code> }` (the code can't be overridden by an extensions entry), and uses `status_code` as the HTTP status.
-
-## Built-in subclasses
-
-Each fixes a `(code, status)` pair; all share the signature `( string $message = <default>, array $extensions = [], ?\Throwable $previous = null )`.
-
-| Class | `extensions.code` | HTTP | Use when |
-| --- | --- | --- | --- |
-| `UnauthorizedException` | `UNAUTHORIZED` | 401 | Authentication is required but missing; or a generic auth denial where re-authenticating might help. |
-| `InvalidTokenException` | `INVALID_TOKEN` | 401 | Credentials were supplied but rejected (bad/expired token, malformed header). |
-| `ForbiddenException` | `FORBIDDEN` | 403 | Authenticated, but lacks permission ("I know who you are, but you can't do this") |
-| `NotFoundException` | `NOT_FOUND` | 404 | The resource doesn't exist. (When existence is sensitive, prefer `UnauthorizedException` to avoid leaking it.) |
-| `ValidationException` | `VALIDATION_ERROR` | 422 | Input is well-formed but fails a business rule. |
-
-## Other translated throwables
-
-| Thrown | Becomes |
-| --- | --- |
-| `\InvalidArgumentException` | `INVALID_ARGUMENT` / 400 - use for malformed/structural input (wrong type, contradictory args). |
-| any other `\Throwable` | `INTERNAL_ERROR` / 500 - message masked; the original is attached as `previous` and shown only in debug mode. |
-
-The framework also maps engine-level issues itself (e.g. an out-of-range `Int` output → `BAD_USER_INPUT` / 400; depth/complexity violations → 400).
-
-## Authorization-failure status
-
-When an authorization gate denies (rather than throwing), the framework picks the status from the principal: **401 `UNAUTHORIZED`** for anonymous principals (`is_authenticated()` is `false`), **403 `FORBIDDEN`** for authenticated ones or principals that don't expose `is_authenticated()`. See [Authentication and authorization](../authentication-and-authorization.md).
-
-## Creating a custom exception (in a plugin or core)
-
-Extend `ApiException` (or a subclass when its behavior fits) and pin your own code and status:
-
-```php
-namespace Automattic\MyPlugin\Api;
-
-use Automattic\WooCommerce\Api\ApiException;
-
-class QuotaExceededException extends ApiException {
- public function __construct( string $message = 'Quota exceeded.', array $extensions = array(), ?\Throwable $previous = null ) {
- parent::__construct( $message, 'QUOTA_EXCEEDED', $extensions, 429, $previous );
- }
-}
-```
-
-Throw it from a command; the `code` and `status_code` surface automatically, and any `extensions` you pass appear alongside `code` in the response. Use a sensible standard HTTP status for your domain.
diff --git a/docs/apis/dual-api/reference/infrastructure-classes.md b/docs/apis/dual-api/reference/infrastructure-classes.md
deleted file mode 100644
index c41db452272..00000000000
--- a/docs/apis/dual-api/reference/infrastructure-classes.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-post_title: 'Reference: infrastructure classes'
-sidebar_label: 'Infrastructure classes'
-sidebar_position: 4
----
-
-# Reference: infrastructure classes
-
-These classes live in `Automattic\WooCommerce\Api\Infrastructure` (and `Api\Utils`). Some are **convention classes** that ApiBuilder detects per plugin; the rest are runtime helpers. Plugin override rules apply equally when adjusting core's own behavior.
-
-## Convention classes
-
-ApiBuilder looks for these at `<api-namespace>\Infrastructure\*` and wires whatever it finds into the generated controller. Ship one only to diverge from the default; otherwise the default applies. The signature must match exactly.
-
-### `ClassResolver`
-
-```php
-public static function resolve_class( string $class_name ): object
-```
-
-Instantiates command and infrastructure classes. **Default:** `wc_get_container()->get( $class_name )`. Ship your own to route through a different DI container. When no resolver is present at all, generated resolvers fall back to `new $class_name()`.
-
-### `PrincipalResolver`
-
-```php
-public function resolve_principal(): Principal
-// or
-public function resolve_principal( \WP_REST_Request $request ): Principal
-```
-
-Resolves the per-request principal once. **Default:** returns `new Principal( wp_get_current_user() )` (no `$request` parameter). The **return type declares the plugin's principal type**, which the builder uses to type-check `authorize()`/`$_principal` against. Throw `UnauthorizedException`/`InvalidTokenException` to reject credentials. Anonymous requests are a resolved principal (not `null`).
-
-### `Principal`
-
-The default principal wraps a `WP_User`:
-
-```php
-public function __construct( public readonly \WP_User $user )
-public function is_authenticated(): bool // user->ID > 0
-public function can_introspect(): bool // user_can( $user, 'manage_woocommerce' )
-public function can_use_debug_mode(): bool // user_can( $user, 'manage_options' )
-```
-
-A custom principal can be any class. Recognized (all optional, duck-typed) methods:
-
-| Method | If declared | If absent |
-| --- | --- | --- |
-| `is_authenticated(): bool` | distinguishes 401 vs 403 on denial; used by your own code | denials default to 403 (`FORBIDDEN`) |
-| `can_introspect(): bool` | gates native introspection (and `_apiMetadata`, as fallback) | introspection denied |
-| `can_use_debug_mode(): bool` | gates debug mode (with `_debug=1`) | debug mode denied |
-| `can_query_metadata(): bool` | gates `_apiMetadata` specifically | falls back to `can_introspect()`, else deny |
-
-Core's `Principal` deliberately omits `can_query_metadata()`, so `_apiMetadata` follows `can_introspect()`.
-
-### `HttpStatusResolver`
-
-```php
-public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int
-```
-
-Optional. Override the framework-computed HTTP status for any response (e.g. always 200), or return `$default_status` to defer. Called for both success and error responses. **Must not throw**: any throw is converted to a fixed 500 `INTERNAL_ERROR`. **Default:** core ships none, so its per-error-code mapping applies. See [Settings and caching](../caching-and-settings.md).
-
-## Runtime helpers
-
-You generally don't call these directly (generated code does), but they're public for advanced use.
-
-### `GraphQLControllerBase`
-
-Abstract base for the generated controller; owns the request lifecycle. Notable public members:
-
-- `get_schema(): SchemaHandle`: schema handle for metadata inspection.
-- `build_schema(): Schema\Schema`: returns the engine-decoupled wrapper, never the engine type.
-- Static config accessors `get_endpoint_url()`, `get_max_query_depth()`, `get_max_query_complexity()`.
-
-### `ResolverHelpers`
-
-Static helpers used by generated resolvers: exception translation, pagination construction, authorization checks, and the public `compute_preauthorized( string $command_fqcn, object $principal ): bool`.
-
-### `Main`
-
-Bootstrap and registration:
-
-- `is_enabled(): bool`: checks PHP 8.1+ and the `dual_code_graphql_api` flag.
-- `register_graphql_endpoint( string $plugin_dir_or_controller_class, string $route_namespace, string $route, array $methods = ['GET','POST'] ): void`: register a plugin endpoint. No-op when the feature is off.
-- `instantiate_graphql_controller( string $controller_class_name ): ?GraphQLControllerBase`.
-
-### `MetadataController`, `QueryInfoExtractor`
-
-Hand-written runtime pieces: the `_apiMetadata` field/types, and the `ResolveInfo` → `_query_info` extraction. See [Extending the infrastructure](../extending-the-infrastructure.md).
-
-## `SchemaHandle` (`Api\Utils`)
-
-Opaque, engine-independent handle returned by `get_schema()`:
-
-```php
-public function get_all_metadata(): array
-public function find_metadata( ?string $name = null, ?string $type = null, ?string $field = null ): array
-```
-
-## Utility classes
-
-Plain helpers (mappers, repositories) live under `Api/Utils/` and are not exposed in the schema, e.g. `Utils\Products\ProductRepository` (`find( int $id ): ?\WC_Product`, `save( \WC_Product $product ): void`). Inject them into commands via the `ClassResolver`/DI container. Plugins place their own helpers under their `Api/Utils/`.
diff --git a/docs/apis/dual-api/reference/recognized-methods-and-parameters.md b/docs/apis/dual-api/reference/recognized-methods-and-parameters.md
deleted file mode 100644
index 44b76d167e6..00000000000
--- a/docs/apis/dual-api/reference/recognized-methods-and-parameters.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-post_title: 'Reference: recognized methods and parameters'
-sidebar_label: 'Methods and parameters'
-sidebar_position: 3
----
-
-# Reference: recognized methods and parameters
-
-The builder recognizes certain method names on command and attribute classes, and certain specially named parameters that it injects at runtime. These conventions are identical for core and plugins.
-
-## Methods on command classes (queries/mutations)
-
-| Method | Signature | Notes |
-| --- | --- | --- |
-| `execute` | `execute( ...args ): <return type>` | Required. Parameters become GraphQL arguments; the return type becomes the GraphQL return type (use `#[ReturnType]` for interface returns). |
-| `authorize` | `authorize( ...args ): bool` | Optional. Custom authorization for the operation; return `false` to deny. Compose with attributes via `$_preauthorized`. |
-
-## Methods on attribute classes
-
-| Method | Signature | Makes the attribute… |
-| --- | --- | --- |
-| `authorize` | `authorize( <PrincipalType> $principal, ... ): bool` | an authorization attribute. |
-| `get_name` / `get_value` | `get_name(): string` / `get_value(): bool\|int\|float\|string\|null` | (on `Metadata` subclasses) expose the metadata entry. |
-| `transform_description` | `transform_description( string $description ): string` | a description-mirroring metadata attribute. |
-| `shows_in_metadata_query` | `shows_in_metadata_query(): bool` | able to opt its target out of `_apiMetadata` (when it returns `false`). |
-
-## Methods on custom scalar classes
-
-| Method | Signature | Purpose |
-| --- | --- | --- |
-| `serialize` | `static serialize( mixed $value ): string` | PHP value → transport string. |
-| `parse` | `static parse( string $value ): mixed` | Client string → PHP value; throw `\InvalidArgumentException` on bad input. |
-
-## Recognized parameters
-
-These are optional, underscore-prefixed parameters detected **by name**. They may appear in any order; declare only the ones you use. The underscore prefix also keeps them out of the GraphQL argument list. (`provided_fields` on input types uses the same underscore-invisibility idea for an internal property.)
-
-| Parameter | Type | Available on | Value |
-| --- | --- | --- | --- |
-| `$_principal` | the registered principal type | `execute()`, `authorize()` | The resolved principal for the request. |
-| `$_preauthorized` | `bool` | `authorize()` (command) | Whether the attribute-based gates already grant access — compose your custom check on top. |
-| `$_query_info` | `?array` | `execute()` | The selection tree of the current query, for resolve-time optimization. Provided via `QueryInfoExtractor`. |
-| `$_metadata` | `array` | `authorize()` (attribute) | `#[Metadata]` entries at the call site, in slices `['query']`, `['type']`, `['field']` (each `array<string, scalar>`). At the operation level only `['query']` is populated. |
-| `$_args` | `array` | `authorize()` (attribute) | The GraphQL arguments at the call site. |
-| `$_parent` | `mixed` | `authorize()` (attribute) | The enclosing object being resolved, for output-field gates (enables owner-or-scope checks). |
-
-For how these combine in granular authorization, see [Authentication and authorization](../authentication-and-authorization.md).
-
-## Public PHP-side helpers
-
-| Call | Purpose |
-| --- | --- |
-| `ResolverHelpers::compute_preauthorized( string $command_fqcn, object $principal ): bool` | Ask whether attribute gates would grant access, without executing the command. |
-| `GraphQLControllerBase::get_schema(): SchemaHandle` | Obtain the schema handle for PHP-side metadata inspection. |
-| `SchemaHandle::get_all_metadata()` / `find_metadata( ?name, ?type, ?field )` | Read collected metadata. See [Metadata and discovery](../metadata.md). |
diff --git a/plugins/woocommerce/.distignore b/plugins/woocommerce/.distignore
index 5fdbaaeed1b..4f302bd5f29 100644
--- a/plugins/woocommerce/.distignore
+++ b/plugins/woocommerce/.distignore
@@ -52,4 +52,3 @@ webpack.config.js
phpstan.neon
phpstan-baseline.neon
/php-stubs/
-/bin/api-builder/
diff --git a/plugins/woocommerce/bin/api-builder/ApiBuilder.php b/plugins/woocommerce/bin/api-builder/ApiBuilder.php
deleted file mode 100644
index b8dd394b835..00000000000
--- a/plugins/woocommerce/bin/api-builder/ApiBuilder.php
+++ /dev/null
@@ -1,3300 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\DesignTime;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Attributes\Deprecated;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Attributes\Metadata;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\Parameter;
-use Automattic\WooCommerce\Api\Attributes\ParameterDescription;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ReturnType;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\Attributes\Unroll;
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-
-/**
- * Scans the public API classes and generates the GraphQL schema and resolver code.
- *
- * The builder is parameterised so the same infrastructure can be reused by
- * sibling WooCommerce plugins that ship their own code-API classes. Each
- * caller supplies the input directory/namespace to scan and the output
- * directory/namespace to generate into. When invoked without arguments the
- * constructor defaults reproduce WooCommerce core's own configuration, so
- * core's build pipeline is unaffected.
- */
-class ApiBuilder {
- private const TEMPLATES_DIR = __DIR__ . '/code-templates';
-
- private string $api_dir;
- private string $autogenerated_dir;
- private string $api_namespace;
- private string $autogenerated_namespace;
- private ?string $composer_working_dir;
- private string $phpcbf_path;
-
- /**
- * @param ?string $api_dir Absolute path to the directory containing the code-API classes to scan. Null = WooCommerce core's `src/Api`.
- * @param ?string $autogenerated_dir Absolute path to the directory where generated code will be written. The directory is wiped on each build. Null = WooCommerce core's `src/Internal/Api/Autogenerated`.
- * @param ?string $api_namespace PSR-4 namespace that maps to $api_dir. Null = `Automattic\WooCommerce\Api`.
- * @param ?string $autogenerated_namespace PSR-4 namespace that maps to $autogenerated_dir. Null = `Automattic\WooCommerce\Internal\Api\Autogenerated`.
- * @param ?string $composer_working_dir Directory passed to `composer dump-autoload` after generation. Null = skip autoload regeneration (the caller is expected to run it themselves).
- * @param ?string $phpcbf_path Absolute path to a phpcbf executable used to format generated files. Null = use WooCommerce core's vendored phpcbf. To skip linting entirely, pass $skip_linter to build().
- */
- public function __construct(
- ?string $api_dir = null,
- ?string $autogenerated_dir = null,
- ?string $api_namespace = null,
- ?string $autogenerated_namespace = null,
- ?string $composer_working_dir = null,
- ?string $phpcbf_path = null
- ) {
- // The four path/namespace arguments are all-or-nothing. Allowing any
- // subset to default back to WooCommerce core's own values would turn
- // a plugin-side partial configuration into an accidental `wipe` of
- // core's Autogenerated tree (or into a plugin shipping files under
- // core's PSR-4 prefix). If any one is provided, all four must be.
- $provided = array_filter(
- array( $api_dir, $autogenerated_dir, $api_namespace, $autogenerated_namespace ),
- static fn( $v ) => null !== $v
- );
- if ( count( $provided ) > 0 && count( $provided ) < 4 ) {
- throw new \InvalidArgumentException(
- 'ApiBuilder: $api_dir, $autogenerated_dir, $api_namespace and $autogenerated_namespace must be provided together (all four or none).'
- );
- }
-
- $this->api_dir = $api_dir ?? realpath( __DIR__ . '/../../src/Api' );
- $this->autogenerated_dir = $autogenerated_dir ?? realpath( __DIR__ . '/../../src/Internal/Api/Autogenerated' );
- $this->api_namespace = $api_namespace ?? 'Automattic\\WooCommerce\\Api';
- $this->autogenerated_namespace = $autogenerated_namespace ?? 'Automattic\\WooCommerce\\Internal\\Api\\Autogenerated';
- $this->phpcbf_path = $phpcbf_path ?? realpath( __DIR__ . '/../..' ) . '/vendor/bin/phpcbf';
-
- // Default composer working dir to WooCommerce core's plugin dir when no
- // override is given, so core's existing invocation keeps working. When
- // an external caller passes --api-dir they presumably drive composer
- // themselves, so we leave it null and skip the autoload regeneration.
- if ( null === $api_dir && null === $composer_working_dir ) {
- $composer_working_dir = realpath( __DIR__ . '/../..' );
- }
- $this->composer_working_dir = $composer_working_dir;
- }
-
- /**
- * Configure an ApiBuilder by convention for a sibling WooCommerce plugin.
- *
- * The plugin is expected to keep its code-API classes under
- * `$plugin_root/src/Api` and emit generated code into
- * `$plugin_root/src/Internal/Api/Autogenerated`, with PSR-4 namespaces
- * derived from `$namespace_prefix`. Composer dump-autoload runs in
- * `$plugin_root`; phpcbf defaults to WooCommerce core's vendored copy
- * (callers that want a different style pass --phpcbf-path to the CLI
- * instead of using this factory).
- *
- * @param string $plugin_root Absolute path to the plugin repository root.
- * @param string $namespace_prefix Top-level PSR-4 namespace the plugin publishes under (e.g. 'Automattic\\WooCommerceSimpleMath').
- */
- public static function for_plugin( string $plugin_root, string $namespace_prefix ): self {
- $namespace_prefix = trim( $namespace_prefix, '\\' );
- return new self(
- $plugin_root . '/src/Api',
- $plugin_root . '/src/Internal/Api/Autogenerated',
- $namespace_prefix . '\\Api',
- $namespace_prefix . '\\Internal\\Api\\Autogenerated',
- $plugin_root
- );
- }
-
- /**
- * Turn-key entry point for a sibling plugin's `bin/build-api.php` script.
- *
- * Given the plugin root and namespace prefix, requires the plugin's own
- * composer autoloader, parses `--no-linter` out of `$argv`, configures an
- * ApiBuilder via {@see self::for_plugin()}, and runs the build. Writes a
- * clear error to STDERR and exits non-zero on the two common failure
- * modes (missing plugin autoloader, builder call fails).
- *
- * Keeps the plugin-side script tiny: after locating WooCommerce and
- * requiring WooCommerce's own autoloader, the plugin only needs to call
- * this method with its own root path and namespace prefix.
- *
- * @param string $plugin_root Absolute path to the plugin repository root.
- * @param string $namespace_prefix Top-level PSR-4 namespace the plugin publishes under.
- */
- public static function run_for_plugin( string $plugin_root, string $namespace_prefix ): void {
- if ( ! is_file( $plugin_root . '/vendor/autoload.php' ) ) {
- fwrite( STDERR, "Plugin autoloader not found at {$plugin_root}/vendor/autoload.php. Run `composer install` in the plugin root first.\n" );
- exit( 1 );
- }
- require_once $plugin_root . '/vendor/autoload.php';
-
- $argv = $GLOBALS['argv'] ?? array();
- $skip_linter = in_array( '--no-linter', $argv, true );
-
- try {
- self::for_plugin( $plugin_root, $namespace_prefix )->build( $skip_linter );
- } catch ( \Throwable $e ) {
- fwrite( STDERR, "API build failed: {$e->getMessage()}\n" );
- exit( 1 );
- }
- }
-
- /** @var array<string, array{class: \ReflectionClass|\ReflectionEnum, kind: string, ignored: bool}> */
- private array $classes = array();
-
- /** @var array<string, string> Map of PHP FQCN => GraphQL name */
- private array $graphql_names = array();
-
- /** @var string[] Errors collected during validation */
- private array $errors = array();
-
- /**
- * Cache of harvested metadata, keyed by the context label that
- * {@see self::harvest_metadata()} is called with. Populated by the
- * pre-generate {@see self::validate_metadata()} pass and re-used by
- * the generate phase, so duplicate-name errors surface before the
- * autogenerated tree is wiped and each conflict is only recorded once.
- *
- * @var array<string, array<string, bool|int|float|string|null>>
- */
- private array $metadata_cache = array();
-
- /** @var string[] Warnings collected during build */
- private array $warnings = array();
-
- /** @var array Discovered connections: ['node_type' => FQCN, 'source' => string] */
- private array $connections = array();
-
- /** @var array<string, string[]> Map of interface trait FQCN => list of output type FQCNs that use it */
- private array $interface_implementors = array();
-
- /** @var ?string Optional FQCN of a user-provided `<api_namespace>\Infrastructure\ClassResolver` class with a public static `resolve_class(string): object` method; null when absent. */
- private ?string $class_resolver_fqcn = null;
-
- /** @var ?string Optional FQCN of a user-provided `<api_namespace>\Infrastructure\PrincipalResolver` class; null when absent. */
- private ?string $principal_resolver_fqcn = null;
-
- /** @var ?string Optional FQCN of a user-provided `<api_namespace>\Infrastructure\HttpStatusResolver` class with a public `resolve_status(int, array, \WP_REST_Request): int` method; null when absent. */
- private ?string $status_resolver_fqcn = null;
-
- /**
- * Whether the detected PrincipalResolver's `resolve_principal()` declares the
- * \WP_REST_Request parameter (true) or omits it (false).
- *
- * Captured at build time so the generated controller subclass can call the
- * resolver with the right arity without runtime reflection. Meaningless when
- * {@see self::$principal_resolver_fqcn} is null.
- */
- private bool $principal_resolver_takes_request = false;
-
- /**
- * Principal type captured from the detected PrincipalResolver's `resolve_principal()` return type.
- *
- * Either `'object'` (when a plugin's resolver explicitly returns `object`) or a class FQCN.
- * Used to type-check `_principal` parameters across commands and authorization attributes.
- * Defaults to {@see Principal} — the principal class shipped by core that the controller's
- * fallback returns when no plugin `PrincipalResolver` is detected — so the build-time check
- * matches the runtime payload (a `_principal: WP_User` parameter would otherwise validate
- * here but `TypeError` at runtime).
- *
- * @var string
- */
- private string $principal_type = Principal::class;
-
- /**
- * Map of FQCN => per-attribute slot flags for every autodiscovered authorization attribute.
- *
- * An attribute class qualifies when it lives in `Api/Attributes/` and declares an
- * `authorize()` method whose signature matches the contract enforced by
- * {@see self::validate_attribute_authorize_shape()}. Each flag records whether the
- * `authorize()` method declared the corresponding opt-in slot, so call-site
- * emission can thread only the slots the attribute asked for:
- *
- * - `takes_principal` — the principal positional parameter.
- * - `takes_metadata` — `array $_metadata` (the harvested metadata slices).
- * - `takes_args` — `array $_args` (the GraphQL field arguments at the call site).
- * - `takes_parent` — `mixed $_parent` (the parent value being resolved).
- *
- * Populated by {@see self::discover_authorization_attributes()}.
- *
- * @var array<class-string, array{takes_principal: bool, takes_metadata: bool, takes_args: bool, takes_parent: bool}>
- */
- private array $authorization_attribute_fqcns = array();
-
- // Counters for summary.
- private int $query_count = 0;
- private int $mutation_count = 0;
- private int $type_count = 0;
- private int $input_type_count = 0;
- private int $enum_count = 0;
- private int $scalar_count = 0;
- private int $interface_count = 0;
-
- public function build( bool $skip_linter = false ): void {
- echo "Scanning {$this->api_dir} for code API classes...\n";
-
- $this->detect_class_resolver();
- $this->detect_principal_resolver();
- $this->detect_status_resolver();
- $this->discover_authorization_attributes();
- $this->discover();
- $this->check_attribute_resolutions();
- $this->validate();
- $this->validate_metadata();
-
- if ( ! empty( $this->errors ) ) {
- fwrite( STDERR, "Build failed with errors:\n" );
- foreach ( $this->errors as $error ) {
- fwrite( STDERR, " - {$error}\n" );
- }
- exit( 1 );
- }
-
- $this->wipe_autogenerated();
- $this->create_directory_structure();
- $this->generate();
- if ( ! $skip_linter ) {
- echo "Applying linter to generated files...\n";
- $this->format_with_phpcbf( $this->autogenerated_dir );
- }
- $this->write_timestamp();
-
- if ( null !== $this->composer_working_dir ) {
- echo "Regenerating autoloader...\n";
- // Use chdir() + passthru() instead of exec() with --working-dir.
- // Some composer post-autoload-dump hooks — notably the Jetpack
- // autoloader's merged-manifest step that WooCommerce relies on
- // at runtime — read getcwd() rather than honouring composer's
- // resolved --working-dir, and silently produce stale manifests
- // when the two don't match. Setting the actual process CWD
- // keeps every hook on the same dir; passthru() also streams
- // output so any failure mode is visible to the developer
- // rather than being swallowed into a captured array.
- $original_cwd = getcwd();
- if ( false === chdir( $this->composer_working_dir ) ) {
- echo "Warning: could not chdir to {$this->composer_working_dir}; skipping autoloader regeneration.\n";
- } else {
- // --dev so the autoload-dev PSR-4 entries (notably
- // `Automattic\WooCommerce\Tests\` → `tests/php/src/`) stay
- // registered. Otherwise a subsequent `build:api:test` run
- // can't reflect the dummy-API fixtures and aborts. This is
- // a design-time CLI script; dev autoload is a strict
- // superset of prod, so there's no downside.
- // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_passthru -- design-time CLI script; never runs in a web context.
- passthru( 'composer dump-autoload --dev', $code );
- if ( false !== $original_cwd ) {
- chdir( $original_cwd );
- }
- if ( 0 !== $code ) {
- echo "Warning: composer dump-autoload exited with code {$code}.\n";
- }
- }
- }
-
- // Print summary.
- echo "\n=== Build Complete ===\n";
- echo " Queries: {$this->query_count}\n";
- echo " Mutations: {$this->mutation_count}\n";
- echo " Types: {$this->type_count}\n";
- echo " Input Types: {$this->input_type_count}\n";
- echo " Enums: {$this->enum_count}\n";
- echo " Scalars: {$this->scalar_count}\n";
- echo " Interfaces: {$this->interface_count}\n";
- echo ' Connections: ' . count( $this->connections ) . "\n";
-
- if ( ! empty( $this->warnings ) ) {
- echo "\nWarnings:\n";
- foreach ( $this->warnings as $warning ) {
- echo " - {$warning}\n";
- }
- }
- }
-
- // ========================================================================
- // ClassResolver detection
- // ========================================================================
-
- /**
- * Detect an optional user-provided class resolver.
- *
- * If a class `<api_namespace>\Infrastructure\ClassResolver` exists and exposes
- * a `public static function resolve_class(string): object` method, autogenerated
- * resolvers route command lookups through it. Other infrastructure classes
- * recognised by convention (currently {@see PrincipalResolver}) are also
- * resolved through it. Otherwise commands fall back to `new $command_class()`.
- *
- * WooCommerce core ships such a resolver at
- * `Automattic\WooCommerce\Api\Infrastructure\ClassResolver` delegating to
- * `wc_get_container()`. Sibling plugins can ship their own with the same signature.
- *
- * Misshapen explicit-registration: a class at the conventional path that does
- * not match the expected signature is a hard build-time error rather than a
- * silent fallback — auth-related infrastructure is too important to let a
- * typo route through the default.
- */
- private function detect_class_resolver(): void {
- $candidate = $this->api_namespace . '\\Infrastructure\\ClassResolver';
- if ( ! class_exists( $candidate ) ) {
- return;
- }
- if ( ! method_exists( $candidate, 'resolve_class' ) ) {
- $this->errors[] = "ClassResolver class {$candidate} has no resolve_class() method.";
- return;
- }
- $method = new \ReflectionMethod( $candidate, 'resolve_class' );
- if ( ! $method->isStatic() || ! $method->isPublic() ) {
- $this->errors[] = "ClassResolver class {$candidate}::resolve_class() must be public static.";
- return;
- }
-
- // Validate signature `resolve_class(string): object`. A wrong signature would pass
- // the checks above but fail at request time inside the generated
- // resolvers with a type error far removed from its cause.
- $params = $method->getParameters();
- $return_type = $method->getReturnType();
- $param_type = isset( $params[0] ) ? $params[0]->getType() : null;
- $single_required_string_param = 1 === count( $params )
- && ! $params[0]->isVariadic()
- && ! $params[0]->isOptional()
- && $param_type instanceof \ReflectionNamedType
- && 'string' === $param_type->getName();
- $object_return = $return_type instanceof \ReflectionNamedType && 'object' === $return_type->getName();
- if ( ! $single_required_string_param || ! $object_return ) {
- $this->errors[] = "ClassResolver class {$candidate}::resolve_class() must have signature `resolve_class(string): object`.";
- return;
- }
-
- $this->class_resolver_fqcn = $candidate;
- echo " Using class resolver: {$candidate}.\n";
- }
-
- /**
- * Detect an optional user-provided principal resolver.
- *
- * If a class `<api_namespace>\Infrastructure\PrincipalResolver` exists and exposes
- * a `resolve_principal(): T` or `resolve_principal( \WP_REST_Request ): T`
- * method, autogenerated controllers route per-request principal resolution
- * through it. Otherwise the controller falls back to a default
- * {@see \Automattic\WooCommerce\Api\Infrastructure\Principal} wrapping
- * `wp_get_current_user()`.
- *
- * The return type (a class FQCN, or `object`) becomes the principal type
- * used to type-check `_principal` parameters across commands and
- * authorization attributes. The return type must be non-null — anonymous
- * requests are represented by a sentinel principal (e.g. a Principal whose
- * underlying WP_User has `ID === 0`); the resolver never returns null.
- *
- * The request parameter is optional — resolvers that don't need to inspect
- * the request (the default WP-user resolver, for example) can declare a
- * zero-arg method. The runtime call site reads the actual arity via the
- * autogenerated controller's `principal_resolver_takes_request()` override.
- *
- * Misshapen explicit-registration is a hard build-time error (see
- * {@see self::detect_class_resolver()} for the rationale).
- */
- private function detect_principal_resolver(): void {
- $candidate = $this->api_namespace . '\\Infrastructure\\PrincipalResolver';
- if ( ! class_exists( $candidate ) ) {
- return;
- }
- if ( ! method_exists( $candidate, 'resolve_principal' ) ) {
- $this->errors[] = "PrincipalResolver class {$candidate} has no resolve_principal() method.";
- return;
- }
- $method = new \ReflectionMethod( $candidate, 'resolve_principal' );
- if ( $method->isStatic() || ! $method->isPublic() ) {
- $this->errors[] = "PrincipalResolver class {$candidate}::resolve_principal() must be a public non-static method.";
- return;
- }
-
- // Validate signature: zero parameters, OR a single \WP_REST_Request parameter.
- // Return type must be non-null — anonymous is signalled by a sentinel
- // principal, not by null. The captured type is used at request time to
- // type-check `_principal` parameters when WordPress IS loaded.
- $params = $method->getParameters();
- $return_type = $method->getReturnType();
-
- if ( count( $params ) > 1 ) {
- $this->errors[] = "PrincipalResolver class {$candidate}::resolve_principal() must take 0 parameters or a single \\WP_REST_Request parameter; got " . count( $params ) . '.';
- return;
- }
- if ( 1 === count( $params ) ) {
- $param_type = $params[0]->getType();
- $valid_request_param = ! $params[0]->isVariadic()
- && ! $params[0]->isOptional()
- && $param_type instanceof \ReflectionNamedType
- && 'WP_REST_Request' === ltrim( $param_type->getName(), '\\' );
- if ( ! $valid_request_param ) {
- $this->errors[] = "PrincipalResolver class {$candidate}::resolve_principal() parameter must be typed as `\\WP_REST_Request`.";
- return;
- }
- }
-
- if ( ! $return_type instanceof \ReflectionNamedType || $return_type->allowsNull() ) {
- $this->errors[] = "PrincipalResolver class {$candidate}::resolve_principal() must declare a non-nullable return type (anonymous requests are represented by a sentinel principal, not null).";
- return;
- }
-
- $return_type_name = $return_type->getName();
-
- // Reject scalar / void-ish keywords. We can't enforce class_exists() at
- // build time because the WordPress runtime — and any of its classes the
- // principal might be typed as, e.g. WP_User — is not loaded by the
- // build script. The string is captured verbatim and used at request
- // time to type-check `_principal` parameters when WordPress IS loaded.
- $rejected_type_names = array( 'void', 'never', 'null', 'mixed', 'string', 'int', 'float', 'bool', 'array', 'iterable', 'callable', 'self', 'static', 'parent' );
- if ( in_array( $return_type_name, $rejected_type_names, true ) ) {
- $this->errors[] = "PrincipalResolver class {$candidate}::resolve_principal() return type must be `object` or `<ClassName>`, got `{$return_type_name}`.";
- return;
- }
-
- $this->principal_resolver_fqcn = $candidate;
- $this->principal_type = $return_type_name;
- $this->principal_resolver_takes_request = 1 === count( $params );
- echo " Using principal resolver: {$candidate} (principal type: {$return_type_name}).\n";
- }
-
- // ========================================================================
- // HTTP status resolver detection
- // ========================================================================
-
- /**
- * Detect an optional plugin-supplied HTTP status resolver.
- *
- * If a class `<api_namespace>\Infrastructure\HttpStatusResolver` exists
- * and exposes a `public function resolve_status( int, array,
- * \WP_REST_Request ): int` instance method, the autogenerated
- * GraphQLController subclass overrides `get_status_resolver()` to return
- * an instance of it. The base controller then routes every status
- * decision through that instance, letting plugins override status codes
- * (e.g. Shopify-style "always 200") without touching the framework.
- *
- * Plugins that omit this class get the framework defaults unchanged.
- * WooCommerce core does not ship a default class.
- */
- private function detect_status_resolver(): void {
- $candidate = $this->api_namespace . '\\Infrastructure\\HttpStatusResolver';
- if ( ! class_exists( $candidate ) ) {
- return;
- }
- if ( ! method_exists( $candidate, 'resolve_status' ) ) {
- $this->warnings[] = "HttpStatusResolver class {$candidate} has no resolve_status() method; ignoring.";
- return;
- }
- $method = new \ReflectionMethod( $candidate, 'resolve_status' );
- if ( ! $method->isPublic() || $method->isStatic() ) {
- $this->warnings[] = "HttpStatusResolver class {$candidate}::resolve_status() must be a public instance method; ignoring.";
- return;
- }
-
- // Validate `resolve_status(int, array, \WP_REST_Request): int`. A
- // wrong signature would slip past these checks and surface as a
- // TypeError deep inside pick_status() at request time, far from its
- // cause.
- $params = $method->getParameters();
- $return_type = $method->getReturnType();
-
- $is_required_named_type = static function ( ?\ReflectionParameter $param, string $expected ): bool {
- if ( null === $param || $param->isVariadic() || $param->isOptional() ) {
- return false;
- }
- $type = $param->getType();
- return $type instanceof \ReflectionNamedType && ltrim( $type->getName(), '\\' ) === $expected;
- };
-
- $signature_ok = 3 === count( $params )
- && $is_required_named_type( $params[0], 'int' )
- && $is_required_named_type( $params[1], 'array' )
- && $is_required_named_type( $params[2], 'WP_REST_Request' )
- && $return_type instanceof \ReflectionNamedType
- && 'int' === $return_type->getName();
-
- if ( ! $signature_ok ) {
- $this->warnings[] = "HttpStatusResolver class {$candidate}::resolve_status() must have signature `resolve_status(int, array, \\WP_REST_Request): int`; ignoring.";
- return;
- }
-
- $this->status_resolver_fqcn = $candidate;
- echo " Using HTTP status resolver: {$candidate}.\n";
- }
-
- // ========================================================================
- // Authorization attribute discovery
- // ========================================================================
-
- /**
- * Discover the set of attribute classes that participate in autodiscovered
- * authorization.
- *
- * An attribute class qualifies when it declares an `authorize()` method
- * whose signature is one of:
- * - `authorize( ?<PrincipalType> $principal ): bool`
- * - `authorize( ?object $principal ): bool`
- * - `authorize(): bool`
- *
- * The set is the union of:
- *
- * 1. WooCommerce core's well-known authorization attributes
- * ({@see RequiredCapability}, {@see PublicAccess}) — always included so
- * sibling plugin builds inherit them without redeclaring.
- * 2. Attributes discovered by scanning `<api_dir>/Attributes/` for classes
- * that declare an authorize() method.
- *
- * Plain attributes without an `authorize()` method (e.g. {@see \Automattic\WooCommerce\Api\Attributes\Description})
- * are skipped. Misshapen `authorize()` methods are a hard build-time error.
- */
- private function discover_authorization_attributes(): void {
- // 1. Well-known core authorization attributes — always available via the
- // shared autoloader, regardless of whether the build is for core itself
- // or a sibling plugin. Auto-included with `is_core = true`, so a plugin
- // whose principal type makes a core attribute inapplicable (e.g. a
- // non-WP-User principal vs. RequiredCapability's ?WP_User) gets it
- // silently skipped rather than a hard build error from infrastructure
- // the plugin didn't even ask for.
- $core_attribute_fqcns = array(
- RequiredCapability::class,
- PublicAccess::class,
- );
- foreach ( $core_attribute_fqcns as $fqcn ) {
- $this->try_register_authorization_attribute( $fqcn, true );
- }
-
- // 2. Plugin-supplied attributes living under <api_dir>/Attributes.
- // `is_core = false`: principal-type incompatibility on a plugin-owned
- // attribute is a developer error and surfaces as a build failure.
- $attributes_dir = $this->api_dir . '/Attributes';
- if ( ! is_dir( $attributes_dir ) ) {
- return;
- }
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator( $attributes_dir, \FilesystemIterator::SKIP_DOTS )
- );
- foreach ( $iterator as $file ) {
- if ( 'php' !== $file->getExtension() ) {
- continue;
- }
- $fqcn = $this->file_to_fqcn( $file->getPathname() );
- if ( null === $fqcn ) {
- continue;
- }
- $this->try_register_authorization_attribute( $fqcn, false );
- }
- }
-
- /**
- * Register an attribute class as an authorization attribute when its
- * `authorize()` signature matches the contract; do nothing otherwise.
- *
- * @param string $fqcn FQCN of the candidate attribute class.
- * @param bool $is_core True for core attributes auto-included regardless of whether the
- * plugin uses them — principal-type incompatibility is silent here so
- * a non-WP-User plugin doesn't fail the build over a core attribute it
- * never imported. False for plugin-supplied attributes; principal-type
- * incompatibility surfaces as a developer-facing build error.
- */
- private function try_register_authorization_attribute( string $fqcn, bool $is_core ): void {
- if ( ! class_exists( $fqcn ) ) {
- return;
- }
- $ref = new \ReflectionClass( $fqcn );
- if ( ! $ref->hasMethod( 'authorize' ) ) {
- return;
- }
- $method = $ref->getMethod( 'authorize' );
- $validated = $this->validate_attribute_authorize_shape( $fqcn, $method );
- if ( null === $validated ) {
- return;
- }
- if ( null !== $validated['principal_param']
- && ! $this->is_principal_type_compatible( self::param_type_name( $validated['principal_param'] ) )
- ) {
- if ( ! $is_core ) {
- $this->record_attribute_principal_mismatch_error( $fqcn, $validated['principal_param'] );
- }
- return;
- }
- $this->authorization_attribute_fqcns[ $fqcn ] = array(
- 'takes_principal' => null !== $validated['principal_param'],
- 'takes_metadata' => $validated['takes_metadata'],
- 'takes_args' => $validated['takes_args'],
- 'takes_parent' => $validated['takes_parent'],
- );
- }
-
- /**
- * Validate the *shape* of an attribute's `authorize()` method — return type
- * and per-parameter contract — independent of principal-type compatibility.
- *
- * Accepted parameters (any order, any subset):
- * - One positional principal parameter: any name that does not start with
- * `_`. Must be non-nullable and typed. Type compatibility with the
- * registered principal type is checked separately by the caller.
- * - `array $_metadata`: opt-in metadata slices (`query`, `type`, `field`).
- * - `array $_args`: opt-in GraphQL field arguments at the call site.
- * - `mixed $_parent` (or untyped): opt-in parent value at the call site.
- *
- * Any other `_`-prefixed parameter name, more than one principal candidate,
- * a wrongly typed infra parameter, or a malformed return type produces a
- * build-time error.
- *
- * @return array{principal_param: ?\ReflectionParameter, takes_metadata: bool, takes_args: bool, takes_parent: bool}|null
- * Structured per-parameter classification on success, or `null` on validation failure.
- */
- private function validate_attribute_authorize_shape( string $fqcn, \ReflectionMethod $method ): ?array {
- if ( $method->isStatic() || ! $method->isPublic() ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() must be a public non-static method.";
- return null;
- }
-
- $return_type = $method->getReturnType();
- if ( ! $return_type instanceof \ReflectionNamedType || 'bool' !== $return_type->getName() ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() must declare a `bool` return type.";
- return null;
- }
-
- $principal_param = null;
- $takes_metadata = false;
- $takes_args = false;
- $takes_parent = false;
-
- foreach ( $method->getParameters() as $param ) {
- $name = $param->getName();
-
- if ( '_metadata' === $name ) {
- if ( ! self::param_type_is_named( $param, 'array' ) ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() parameter \$_metadata must be typed `array`; got `" . self::param_type_name( $param ) . '`.';
- return null;
- }
- $takes_metadata = true;
- continue;
- }
-
- if ( '_args' === $name ) {
- if ( ! self::param_type_is_named( $param, 'array' ) ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() parameter \$_args must be typed `array`; got `" . self::param_type_name( $param ) . '`.';
- return null;
- }
- $takes_args = true;
- continue;
- }
-
- if ( '_parent' === $name ) {
- $takes_parent = true;
- continue;
- }
-
- if ( '' !== $name && '_' === $name[0] ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() has unknown infrastructure parameter \${$name}; accepted: \$_metadata, \$_args, \$_parent.";
- return null;
- }
-
- if ( null !== $principal_param ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() may declare at most one principal parameter; got \${$principal_param->getName()} and \${$name}.";
- return null;
- }
-
- $param_type = $param->getType();
- $type_name = self::param_type_name( $param );
- if ( ! $param_type instanceof \ReflectionNamedType || $param_type->allowsNull() ) {
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() principal parameter must be non-nullable (anonymous requests are represented by a sentinel principal, not null); got `?{$type_name}`.";
- return null;
- }
-
- $principal_param = $param;
- }
-
- return array(
- 'principal_param' => $principal_param,
- 'takes_metadata' => $takes_metadata,
- 'takes_args' => $takes_args,
- 'takes_parent' => $takes_parent,
- );
- }
-
- /**
- * Record a build error for a plugin-shipped attribute whose `authorize()`
- * principal parameter type is incompatible with the registered principal type.
- */
- private function record_attribute_principal_mismatch_error( string $fqcn, \ReflectionParameter $principal_param ): void {
- $type_name = self::param_type_name( $principal_param );
- $expected = 'object' === $this->principal_type
- ? '`object` (or any object class)'
- : "`{$this->principal_type}` (or `object`)";
- $this->errors[] = "Authorization attribute {$fqcn}::authorize() principal parameter must be typed as {$expected}; got `{$type_name}`.";
- }
-
- /**
- * Whether a parameter declares the named type (and only the named type).
- *
- * Used by the authorize-method validator to enforce that opt-in
- * infrastructure parameters declare the type the contract expects
- * (`array` for `$_metadata` and `$_args`; `$_parent` has no required type).
- */
- private static function param_type_is_named( \ReflectionParameter $param, string $expected ): bool {
- $type = $param->getType();
- return $type instanceof \ReflectionNamedType && $type->getName() === $expected;
- }
-
- /**
- * Return a printable name for a parameter's declared type.
- *
- * `mixed` is returned for untyped parameters and for union/intersection
- * types; the validator does not currently inspect those shapes.
- */
- private static function param_type_name( \ReflectionParameter $param ): string {
- $type = $param->getType();
- return $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
- }
-
- /**
- * Render a scalar/array value as a valid PHP expression that, when
- * evaluated, reproduces the value. Used by the resolver-template emission
- * to inline build-time-known data (e.g. metadata slices) into the
- * generated code.
- *
- * The output is `var_export()`-style and is later normalised by phpcbf
- * during the generated-file lint pass, so callers do not need to worry
- * about formatting.
- */
- private static function render_php_literal( mixed $value ): string {
- return var_export( $value, true );
- }
-
- // ========================================================================
- // Discovery
- // ========================================================================
-
- private function discover(): void {
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator( $this->api_dir, \FilesystemIterator::SKIP_DOTS )
- );
-
- foreach ( $iterator as $file ) {
- if ( $file->getExtension() !== 'php' ) {
- continue;
- }
-
- $fqcn = $this->file_to_fqcn( $file->getPathname() );
- if ( $fqcn === null ) {
- continue;
- }
-
- $kind = $this->classify_by_namespace( $fqcn );
- if ( $kind === null || $kind === 'attribute' || $kind === 'exception' || $kind === 'infrastructure' ) {
- continue;
- }
-
- try {
- if ( enum_exists( $fqcn ) ) {
- $ref = new \ReflectionEnum( $fqcn );
- } else {
- $ref = new \ReflectionClass( $fqcn );
- }
- } catch ( \ReflectionException $e ) {
- $this->warnings[] = "Could not reflect {$fqcn}: {$e->getMessage()}";
- continue;
- }
-
- $ignored = ! empty( $ref->getAttributes( Ignore::class ) );
-
- // Abstract classes are base classes, not concrete API
- // endpoints or types — skip them automatically.
- if ( ! $ignored && $ref instanceof \ReflectionClass && $ref->isAbstract() ) {
- $ignored = true;
- }
-
- // Traits outside Interfaces/ are helper mixins (e.g.
- // TracksProvidedFields), not concrete types — skip them so they
- // don't end up emitted as empty-field InputObjectTypes. Traits
- // in Interfaces/ model GraphQL interfaces and are legitimate.
- if ( ! $ignored && $ref instanceof \ReflectionClass && $ref->isTrait() && 'interface' !== $kind ) {
- $ignored = true;
- }
-
- $this->classes[ $fqcn ] = array(
- 'class' => $ref,
- 'kind' => $kind,
- 'ignored' => $ignored,
- );
-
- // Compute GraphQL name.
- $name_attr = $ref->getAttributes( Name::class );
- $graphql_name = ! empty( $name_attr )
- ? $name_attr[0]->newInstance()->name
- : $ref->getShortName();
-
- $this->graphql_names[ $fqcn ] = $graphql_name;
- }
-
- echo ' Found ' . count( $this->classes ) . " classes.\n";
- }
-
- private function file_to_fqcn( string $filepath ): ?string {
- $rel = str_replace( realpath( $this->api_dir ) . '/', '', realpath( $filepath ) );
- $rel = str_replace( '.php', '', $rel );
- $rel = str_replace( '/', '\\', $rel );
- return $this->api_namespace . '\\' . $rel;
- }
-
- private function classify_by_namespace( string $fqcn ): ?string {
- $relative = substr( $fqcn, strlen( $this->api_namespace ) + 1 );
- $parts = explode( '\\', $relative );
- $top_dir = $parts[0];
-
- return match ( $top_dir ) {
- 'Queries' => 'query',
- 'Mutations' => 'mutation',
- 'Types' => 'type',
- 'InputTypes' => 'input_type',
- 'Enums' => 'enum',
- 'Interfaces' => 'interface',
- 'Scalars' => 'scalar',
- 'Pagination' => 'pagination',
- 'Attributes' => 'attribute',
- 'Infrastructure' => 'infrastructure',
- default => $fqcn === $this->api_namespace . '\\ApiException' ? 'exception' : null,
- };
- }
-
- // ========================================================================
- // Validation
- // ========================================================================
-
- private function validate(): void {
- foreach ( $this->classes as $fqcn => $info ) {
- if ( $info['ignored'] ) {
- continue;
- }
-
- $ref = $info['class'];
- $kind = $info['kind'];
-
- match ( $kind ) {
- 'query', 'mutation' => $this->validate_command( $fqcn, $ref ),
- 'type' => $this->validate_output_type( $fqcn, $ref ),
- 'input_type' => $this->validate_input_type( $fqcn, $ref ),
- 'enum' => $this->validate_enum( $fqcn, $ref ),
- 'scalar' => $this->validate_scalar( $fqcn, $ref ),
- 'interface' => $this->validate_interface( $fqcn, $ref ),
- default => null,
- };
- }
- }
-
- private function validate_command( string $fqcn, \ReflectionClass $ref ): void {
- // Must have execute method.
- if ( ! $ref->hasMethod( 'execute' ) ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\" must have an execute method.";
- return;
- }
-
- // Authorization check: must have an autodiscovered authorization attribute
- // (e.g. RequiredCapability, PublicAccess, or any plugin-supplied attribute
- // whose authorize() method matches the principal type), or a non-ignored
- // authorize() method on the command itself. Direct attributes on the
- // class take precedence over inherited ones.
- $auth = $this->resolve_authorization( $ref );
- $has_authorize = $ref->hasMethod( 'authorize' )
- && empty( $ref->getMethod( 'authorize' )->getAttributes( Ignore::class ) );
-
- if ( null === $auth['error'] && empty( $auth['usages'] ) && ! $has_authorize ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\" has no authorization attribute (directly or inherited) and no authorize() method.";
- }
-
- if ( null !== $auth['error'] ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\" {$auth['error']}";
- }
-
- $this->check_for_ignored_auth_attribute( $fqcn, $ref );
-
- // ReturnType attribute validation.
- $execute_method = $ref->getMethod( 'execute' );
- $return_type = $execute_method->getReturnType();
- $return_type_name = $return_type instanceof \ReflectionNamedType ? $return_type->getName() : 'mixed';
- $return_type_attr = $execute_method->getAttributes( ReturnType::class );
-
- if ( 'object' === $return_type_name && empty( $return_type_attr ) ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\" returns 'object' but has no #[ReturnType] attribute on execute().";
- }
-
- if ( ! empty( $return_type_attr ) && 'object' !== $return_type_name ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\" has #[ReturnType] on execute() but does not return 'object'.";
- }
-
- if ( ! empty( $return_type_attr ) ) {
- $rt_class = $return_type_attr[0]->newInstance()->type;
- $rt_info = $this->get_class_info( $rt_class );
- if ( null === $rt_info || 'interface' !== $rt_info['kind'] ) {
- $this->errors[] = "Query/Mutation \"{$ref->getShortName()}\": #[ReturnType] references '{$rt_class}' which is not a known interface.";
- }
- }
- }
-
- /**
- * Warn when a class declares both an authorization attribute and an
- * authorize() method directly on itself without opting into composition
- * via the $_preauthorized infrastructure parameter. In that configuration
- * the attribute is silently ignored, which is almost always a bug.
- *
- * Inherited attributes paired with a direct authorize() are intentional
- * (the documented override mechanism) and are not flagged.
- *
- * @param string $fqcn The class fully-qualified name.
- * @param \ReflectionClass $ref The reflection of the class.
- */
- private function check_for_ignored_auth_attribute( string $fqcn, \ReflectionClass $ref ): void {
- $has_direct_auth_attribute = false;
- foreach ( array_keys( $this->authorization_attribute_fqcns ) as $attr_fqcn ) {
- if ( ! empty( $ref->getAttributes( $attr_fqcn ) ) ) {
- $has_direct_auth_attribute = true;
- break;
- }
- }
-
- if ( ! $has_direct_auth_attribute ) {
- return;
- }
-
- if ( ! $ref->hasMethod( 'authorize' ) ) {
- return;
- }
-
- $authorize_method = $ref->getMethod( 'authorize' );
- if ( ! empty( $authorize_method->getAttributes( Ignore::class ) ) ) {
- return;
- }
-
- if ( $authorize_method->getDeclaringClass()->getName() !== $fqcn ) {
- // authorize() is inherited; overriding the attribute is intentional.
- return;
- }
-
- foreach ( $authorize_method->getParameters() as $p ) {
- if ( '_preauthorized' === $p->getName() ) {
- // Developer opted into composition.
- return;
- }
- }
-
- $this->warnings[] = sprintf(
- 'Query/Mutation "%s" declares an authorization attribute and an authorize() method on the same class; the attribute has no effect. Add a `bool $_preauthorized` parameter to authorize() to compose the two.',
- $ref->getShortName()
- );
- }
-
- private function validate_output_type( string $fqcn, \ReflectionClass $ref ): void {
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
- $this->validate_property_type( $prop, 'output', $ref->getShortName() );
- }
- }
-
- private function validate_input_type( string $fqcn, \ReflectionClass $ref ): void {
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
- $this->validate_property_type( $prop, 'input', $ref->getShortName() );
- }
- }
-
- private function validate_property_type( \ReflectionProperty $prop, string $context, string $class_name ): void {
- $type = $prop->getType();
- if ( $type === null ) {
- $this->errors[] = "Property \"{$class_name}::\${$prop->getName()}\" must have a type declaration.";
- return;
- }
-
- if ( $type instanceof \ReflectionNamedType && $type->getName() === 'array' ) {
- if ( empty( $prop->getAttributes( ArrayOf::class ) ) && empty( $prop->getAttributes( ConnectionOf::class ) ) ) {
- $this->errors[] = "Property \"{$class_name}::\${$prop->getName()}\" is typed as array but has no #[ArrayOf] attribute.";
- }
- }
- }
-
- private function validate_enum( string $fqcn, \ReflectionEnum $ref ): void {
- if ( ! $ref->isBacked() ) {
- $this->errors[] = "Enum \"{$ref->getShortName()}\" must be a backed enum (string or int).";
- }
- }
-
- private function validate_scalar( string $fqcn, \ReflectionClass $ref ): void {
- if ( ! $ref->hasMethod( 'serialize' ) || ! $ref->hasMethod( 'parse' ) ) {
- $this->errors[] = "Scalar \"{$ref->getShortName()}\" must have static serialize and parse methods.";
- }
- }
-
- private function validate_interface( string $fqcn, \ReflectionClass $ref ): void {
- if ( ! $ref->isTrait() ) {
- $this->errors[] = "Interface \"{$ref->getShortName()}\" must be a trait.";
- return;
- }
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
- $this->validate_property_type( $prop, 'output', $ref->getShortName() );
- }
- }
-
- /**
- * Resolve the authorization strategy for a query/mutation class.
- *
- * A direct attribute on the class itself takes precedence over inherited ones.
- * Having PublicAccess together with any other authorization attribute on the same
- * class is an error, but a derived class may override an inherited attribute.
- *
- * @param \ReflectionClass $ref The class to inspect.
- *
- * @return array{
- * usages: array<int, array{fqcn: string, args_php: string, is_public_access: bool}>,
- * has_public_access: bool,
- * attribute_expr: string,
- * error: ?string,
- * }
- */
- private function resolve_authorization( \ReflectionClass $ref ): array {
- $direct_usages = $this->collect_authorization_usages( $ref );
-
- $usages = $direct_usages;
- if ( empty( $usages ) ) {
- // No direct attribute — collect from the entire ancestor tree:
- // the parent chain plus each ancestor's traits and interfaces
- // (recursively). All inherited sources contribute as peers; the
- // only thing direct attributes shadow is the inherited tree as a
- // whole. A visited-set guards against trait diamonds and
- // interface inheritance loops.
- $visited = array();
- $stack = array_merge(
- $ref->getParentClass() ? array( $ref->getParentClass() ) : array(),
- $ref->getTraits(),
- $ref->getInterfaces(),
- );
- while ( ! empty( $stack ) ) {
- $source = array_shift( $stack );
- $name = $source->getName();
- if ( in_array( $name, $visited, true ) ) {
- continue;
- }
- $visited[] = $name;
- $usages = array_merge( $usages, $this->collect_authorization_usages( $source ) );
- if ( false !== $source->getParentClass() ) {
- $stack[] = $source->getParentClass();
- }
- $stack = array_merge( $stack, $source->getTraits(), $source->getInterfaces() );
- }
- }
-
- $has_public_access = false;
- $other_count = 0;
- foreach ( $usages as $usage ) {
- if ( $usage['is_public_access'] ) {
- $has_public_access = true;
- } else {
- ++$other_count;
- }
- }
-
- $error = null;
- if ( $has_public_access && $other_count > 0 ) {
- $error = 'cannot have PublicAccess together with any other authorization attribute.';
- }
-
- // Build the PHP expression that the resolver's compute_preauthorized()
- // helper returns. The expression references a local `$principal` (the
- // helper's parameter), so resolve() can pass `$context['principal']`
- // in and code-API callers can pass any principal directly. PublicAccess
- // short-circuits to `true` since its authorize() always returns true;
- // combining it with another attribute is rejected above.
- if ( empty( $usages ) ) {
- $attribute_expr = 'true';
- } elseif ( $has_public_access ) {
- $attribute_expr = 'true';
- } else {
- // Command-level metadata literal — `['query' => harvest_metadata($ref)]`.
- // `$_args` and `$_parent` are field-level concepts; at the command
- // level they carry neutral defaults (`[]` and `null`).
- $query_metadata_literal = self::render_php_literal( array( 'query' => $this->harvest_metadata( $ref, $ref->getShortName() ) ) );
-
- $expressions = array_map(
- function ( $u ) use ( $query_metadata_literal ) {
- $flags = $this->authorization_attribute_fqcns[ $u['fqcn'] ];
- $args = array();
- if ( $flags['takes_principal'] ) {
- $args[] = '$principal';
- }
- if ( $flags['takes_metadata'] ) {
- $args[] = '_metadata: ' . $query_metadata_literal;
- }
- if ( $flags['takes_args'] ) {
- $args[] = '_args: array()';
- }
- if ( $flags['takes_parent'] ) {
- $args[] = '_parent: null';
- }
- return sprintf(
- '( new \%s(%s) )->authorize(%s)',
- $u['fqcn'],
- $u['args_php'],
- empty( $args ) ? '' : ' ' . implode( ', ', $args ) . ' '
- );
- },
- $usages
- );
- $attribute_expr = implode( ' && ', $expressions );
- }
-
- return array(
- 'usages' => $usages,
- 'has_public_access' => $has_public_access,
- 'attribute_expr' => $attribute_expr,
- 'error' => $error,
- 'descriptors' => $this->harvest_authorization_descriptors( $ref ),
- );
- }
-
- /**
- * Collect authorization-attribute usages declared directly on a reflector.
- *
- * Iterates the autodiscovered authorization-attribute FQCN set
- * ({@see self::$authorization_attribute_fqcns}) and reads the runtime args
- * supplied at each usage site, so the generated resolver can construct the
- * attribute with the same arguments.
- *
- * Accepts any reflector that supports `getAttributes()` (class, trait,
- * interface, or property) — class-level discovery, ancestor walks, and
- * field-level discovery share the same scanner.
- *
- * @param \ReflectionClass|\ReflectionProperty $source The reflector to read attributes from.
- *
- * @return array<int, array{fqcn: string, args_php: string, is_public_access: bool}>
- */
- private function collect_authorization_usages( $source ): array {
- $usages = array();
- foreach ( array_keys( $this->authorization_attribute_fqcns ) as $attr_fqcn ) {
- foreach ( $source->getAttributes( $attr_fqcn ) as $attr ) {
- $args = $attr->getArguments();
- $args_php = $this->render_attribute_args_php( $attr_fqcn, $args );
- $usages[] = array(
- 'fqcn' => $attr_fqcn,
- 'args_php' => $args_php,
- 'is_public_access' => ( PublicAccess::class === $attr_fqcn ),
- );
- }
- }
- return $usages;
- }
-
- /**
- * Whether the given reflector (class, property, parameter, …) should
- * appear in the `_apiMetadata` discovery query.
- *
- * The check is per-target and uniform across attribute kinds: walk
- * the reflector's direct attributes, and for each whose class
- * declares a `shows_in_metadata_query(): bool` method, call it. If
- * any returns `false`, the target is hidden — its `metadata` and
- * `authorization` keys are dropped from the emitted config, and
- * {@see \Automattic\WooCommerce\Api\Utils\SchemaHandle::get_all_metadata()}
- * does not emit a row for it.
- *
- * Despite the colloquial name, this is unrelated to native GraphQL
- * introspection (`__schema` / `__type`); those queries continue to
- * expose the schema's shape regardless of this flag. The marker
- * only affects the custom `_apiMetadata` channel.
- *
- * The runtime authorization gate is independent of this check: an
- * authorization attribute whose `shows_in_metadata_query()` returns
- * `false` still runs its `authorize()` method at request time. The
- * stock {@see \Automattic\WooCommerce\Api\Attributes\HiddenFromMetadataQuery}
- * marker is the recommended way to opt a target out without giving
- * the attribute any other behaviour.
- *
- * @param \Reflector $reflector Class, property, parameter, or enum case.
- */
- private function is_target_metadata_visible( \Reflector $reflector ): bool {
- foreach ( $reflector->getAttributes() as $attribute ) {
- $name = $attribute->getName();
- if ( ! class_exists( $name ) || ! method_exists( $name, 'shows_in_metadata_query' ) ) {
- continue;
- }
- $instance = $attribute->newInstance();
- if ( false === $instance->shows_in_metadata_query() ) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Walk a class plus its parents / traits / interfaces (visit-set
- * guarded), invoking `$collect` on each reflector and accumulating the
- * returned descriptors. Shared between
- * {@see self::collect_class_authorization_usages()} and
- * {@see self::collect_class_authorization_descriptors()} so the gate
- * emission and the `_apiMetadata` discovery agree on what counts as
- * "from the type's hierarchy".
- *
- * Direct attributes shadow inherited ones — the same rule the existing
- * command-class resolution uses: if the class itself contributes a
- * non-empty list, ancestors are not walked.
- *
- * @param \ReflectionClass $ref Class to walk.
- * @param callable $collect Maps each reflector to a list.
- *
- * @return list<mixed>
- */
- private function walk_class_hierarchy( \ReflectionClass $ref, callable $collect ): array {
- $direct = $collect( $ref );
- if ( ! empty( $direct ) ) {
- return $direct;
- }
- $visited = array();
- $stack = array_merge(
- $ref->getParentClass() ? array( $ref->getParentClass() ) : array(),
- $ref->getTraits(),
- $ref->getInterfaces(),
- );
- $out = array();
- while ( ! empty( $stack ) ) {
- $source = array_shift( $stack );
- $name = $source->getName();
- if ( in_array( $name, $visited, true ) ) {
- continue;
- }
- $visited[] = $name;
- $out = array_merge( $out, $collect( $source ) );
- if ( false !== $source->getParentClass() ) {
- $stack[] = $source->getParentClass();
- }
- $stack = array_merge( $stack, $source->getTraits(), $source->getInterfaces() );
- }
- return $out;
- }
-
- /**
- * Collect class-level authorization usages from a type class, mirroring
- * {@see self::resolve_authorization()}'s inheritance walk (parent chain,
- * traits, interfaces — visit-set guarded). Direct attributes shadow
- * inherited ones; if the class declares any class-level usages directly,
- * ancestors are not walked.
- *
- * Used by {@see self::build_field_definition()} to AND a type's
- * class-level authorization into every one of its field gates, so an
- * attribute on `class Event` (or on a trait it uses, like an
- * `OrganizerOnlyTrait`) restricts read access to all of `Event`'s
- * fields without having to repeat the attribute on each property.
- *
- * @param \ReflectionClass $ref The type class to read class-level usages from.
- *
- * @return array<int, array{fqcn: string, args_php: string, is_public_access: bool}>
- */
- private function collect_class_authorization_usages( \ReflectionClass $ref ): array {
- return $this->walk_class_hierarchy(
- $ref,
- fn( \ReflectionClass $r ): array => $this->collect_authorization_usages( $r ),
- );
- }
-
- /**
- * Inheritance-aware counterpart to
- * {@see self::harvest_authorization_descriptors()}: walks the class's
- * parents / traits / interfaces so trait-supplied class-level
- * authorization surfaces through `_apiMetadata`, not just through the
- * runtime gate. Direct descriptors shadow inherited ones (same rule
- * as the usages walk).
- *
- * @param \ReflectionClass $ref Class to walk.
- *
- * @return list<array{attribute: string, args: list<mixed>}>
- */
- private function collect_class_authorization_descriptors( \ReflectionClass $ref ): array {
- return $this->walk_class_hierarchy(
- $ref,
- fn( \ReflectionClass $r ): array => $this->harvest_authorization_descriptors( $r ),
- );
- }
-
- /**
- * Harvest authorization-attribute descriptors for the `_apiMetadata`
- * discovery endpoint.
- *
- * Mirrors {@see self::collect_authorization_usages()} but emits
- * `{attribute, args}` records in the shape clients read through
- * `_apiMetadata { authorization { attribute args } }`. The per-target
- * `shows_in_metadata_query()` filter is applied by callers
- * ({@see self::build_field_definition()} and the `generate_*_type`
- * methods) — this method always returns every authorization attribute
- * found on the reflector, including those whose target opts out of
- * the metadata query. The caller decides whether to expose them.
- *
- * @param \ReflectionClass|\ReflectionProperty $reflector Source reflector.
- *
- * @return list<array{attribute: string, args: list<mixed>}>
- */
- private function harvest_authorization_descriptors( $reflector ): array {
- $descriptors = array();
- foreach ( array_keys( $this->authorization_attribute_fqcns ) as $attr_fqcn ) {
- foreach ( $reflector->getAttributes( $attr_fqcn ) as $attr ) {
- $short_name = ( false !== strrpos( $attr_fqcn, '\\' ) )
- ? substr( $attr_fqcn, strrpos( $attr_fqcn, '\\' ) + 1 )
- : $attr_fqcn;
- $descriptors[] = array(
- 'attribute' => $short_name,
- 'args' => array_values( $attr->getArguments() ),
- );
- }
- }
- return $descriptors;
- }
-
- /**
- * Resolve authorization for a single property (output field, input field,
- * or trait property).
- *
- * Field-level gates differ from class-level gates in three ways:
- *
- * - The attribute call expression references the runtime locals
- * `$principal`, `$_metadata`, `$_args`, `$_parent` rather than baking
- * `$_metadata` as a build-time literal. Step-5 emission creates these
- * locals before invoking the expression.
- * - `#[PublicAccess]` placed on a property is a build warning (not a
- * hard error) and is treated as a no-op: it always grants, which is
- * indistinguishable from the default allow-by-default semantics that
- * apply to fields with no authorization attribute.
- * - There is no inheritance walk: a field's gate is the attributes
- * declared directly on the property reflector. Trait-declared
- * properties carry their attributes onto every implementing class
- * naturally through PHP's reflection.
- *
- * @return array{usages: list<array{fqcn: string, args_php: string, is_public_access: bool}>, attribute_expr: string}
- */
- private function resolve_field_authorization( \ReflectionProperty $prop, array $type_level_usages = array(), array $type_level_descriptors = array() ): array {
- $all_usages = $this->collect_authorization_usages( $prop );
- $usages = array();
- foreach ( $all_usages as $usage ) {
- if ( $usage['is_public_access'] ) {
- $label = $prop->getDeclaringClass()->getShortName() . '::$' . $prop->getName();
- $this->warnings[] = "#[PublicAccess] on property {$label}: redundant with allow-by-default field semantics; the attribute is ignored at field level.";
- continue;
- }
- $usages[] = $usage;
- }
-
- // AND in the type-class-level usages. PublicAccess at type level
- // short-circuits to true, so it never contributes to a field gate; skip
- // silently. Other type-level usages are concatenated with the field's
- // own usages; the combined expression becomes the field's effective gate.
- foreach ( $type_level_usages as $usage ) {
- if ( $usage['is_public_access'] ) {
- continue;
- }
- $usages[] = $usage;
- }
-
- // Descriptors advertised through `_apiMetadata` must mirror the
- // *effective* gates: a no-op #[PublicAccess] on a field is dropped from
- // $usages above, so drop it from the descriptors too — otherwise
- // discovery would advertise a gate that never runs. $usages and the
- // harvested descriptors are parallel (one per authorization attribute)
- // and #[PublicAccess] is the only no-op, so keeping the descriptors whose
- // attribute short name survives in $usages preserves every real gate and
- // removes only the ignored ones.
- $effective_shorts = array();
- foreach ( $usages as $u ) {
- $fqcn = $u['fqcn'];
- $short = ( false !== strrpos( $fqcn, '\\' ) ) ? substr( $fqcn, strrpos( $fqcn, '\\' ) + 1 ) : $fqcn;
- $effective_shorts[ $short ] = true;
- }
- $descriptors = array_values(
- array_filter(
- array_merge(
- $this->harvest_authorization_descriptors( $prop ),
- $type_level_descriptors,
- ),
- static fn( array $d ): bool => isset( $effective_shorts[ $d['attribute'] ] ),
- )
- );
-
- if ( empty( $usages ) ) {
- return array(
- 'usages' => array(),
- 'attribute_expr' => 'true',
- 'first_attribute_short' => null,
- 'descriptors' => $descriptors,
- );
- }
-
- $expressions = array_map(
- function ( $u ) {
- $flags = $this->authorization_attribute_fqcns[ $u['fqcn'] ];
- $args = array();
- if ( $flags['takes_principal'] ) {
- $args[] = '$principal';
- }
- if ( $flags['takes_metadata'] ) {
- $args[] = '_metadata: $_metadata';
- }
- if ( $flags['takes_args'] ) {
- $args[] = '_args: $_args';
- }
- if ( $flags['takes_parent'] ) {
- $args[] = '_parent: $_parent';
- }
- return sprintf(
- '( new \%s(%s) )->authorize(%s)',
- $u['fqcn'],
- $u['args_php'],
- empty( $args ) ? '' : ' ' . implode( ', ', $args ) . ' '
- );
- },
- $usages
- );
-
- $first_fqcn = $usages[0]['fqcn'];
- $first_short = ( false !== strrpos( $first_fqcn, '\\' ) )
- ? substr( $first_fqcn, strrpos( $first_fqcn, '\\' ) + 1 )
- : $first_fqcn;
-
- return array(
- 'usages' => $usages,
- 'attribute_expr' => implode( ' && ', $expressions ),
- 'first_attribute_short' => $first_short,
- 'descriptors' => $descriptors,
- );
- }
-
- /**
- * Render an attribute's runtime arguments as a comma-separated PHP literal list.
- *
- * Used by the resolver template to instantiate the attribute with the same
- * arguments declared at the usage site. Handles the scalar/array shapes that
- * PHP allows in attribute arguments via {@see \var_export()}; named arguments
- * are emitted as `name: value` so attributes whose constructors are
- * called with named args still round-trip.
- *
- * @param string $attr_fqcn FQCN of the attribute, for error messages.
- * @param array $args Args from {@see \ReflectionAttribute::getArguments()}.
- */
- private function render_attribute_args_php( string $attr_fqcn, array $args ): string {
- $pieces = array();
- foreach ( $args as $key => $value ) {
- $exported = var_export( $value, true );
- if ( is_string( $key ) ) {
- $pieces[] = "{$key}: {$exported}";
- } else {
- $pieces[] = $exported;
- }
- }
- unset( $attr_fqcn );
- return implode( ', ', $pieces );
- }
-
- // ========================================================================
- // Generation
- // ========================================================================
-
- private function wipe_autogenerated(): void {
- if ( is_dir( $this->autogenerated_dir ) ) {
- $this->rmdir_recursive( $this->autogenerated_dir );
- }
- }
-
- private function create_directory_structure(): void {
- $dirs = array(
- $this->autogenerated_dir,
- $this->autogenerated_dir . '/GraphQLTypes/Output',
- $this->autogenerated_dir . '/GraphQLTypes/Input',
- $this->autogenerated_dir . '/GraphQLTypes/Enums',
- $this->autogenerated_dir . '/GraphQLTypes/Interfaces',
- $this->autogenerated_dir . '/GraphQLTypes/Scalars',
- $this->autogenerated_dir . '/GraphQLTypes/Pagination',
- $this->autogenerated_dir . '/GraphQLQueries',
- $this->autogenerated_dir . '/GraphQLMutations',
- );
-
- foreach ( $dirs as $dir ) {
- if ( ! is_dir( $dir ) ) {
- mkdir( $dir, 0755, true );
- }
- }
- }
-
- private function generate(): void {
- $queries = array();
- $mutations = array();
- $interfaces = array();
- $output_types = array();
-
- // Collect interface trait FQCNs for lookup.
- $interface_fqcns = array();
- foreach ( $this->classes as $fqcn => $info ) {
- if ( ! $info['ignored'] && $info['kind'] === 'interface' ) {
- $interface_fqcns[ $fqcn ] = true;
- }
- }
-
- // Scan output types to build interface_implementors map.
- foreach ( $this->classes as $fqcn => $info ) {
- if ( $info['ignored'] || $info['kind'] !== 'type' ) {
- continue;
- }
- foreach ( $info['class']->getTraits() as $trait ) {
- $trait_fqcn = $trait->getName();
- if ( isset( $interface_fqcns[ $trait_fqcn ] ) ) {
- $this->interface_implementors[ $trait_fqcn ][] = $fqcn;
- }
- }
- }
-
- foreach ( $this->classes as $fqcn => $info ) {
- if ( $info['ignored'] || $info['kind'] === 'pagination' ) {
- continue;
- }
-
- $ref = $info['class'];
- $kind = $info['kind'];
-
- match ( $kind ) {
- 'enum' => $this->generate_enum( $fqcn, $ref ),
- 'scalar' => $this->generate_scalar( $fqcn, $ref ),
- 'interface' => $this->generate_interface( $fqcn, $ref ),
- 'type' => $this->generate_output_type( $fqcn, $ref ),
- 'input_type' => $this->generate_input_type( $fqcn, $ref ),
- 'query' => $queries[ $fqcn ] = $ref,
- 'mutation' => $mutations[ $fqcn ] = $ref,
- default => null,
- };
- }
-
- // Pre-scan connections from queries/mutations (ConnectionOf on execute methods).
- $this->discover_connections( $queries, $mutations );
-
- // Generate connections (PageInfo first since connections reference it).
- $this->generate_page_info();
- foreach ( $this->connections as $conn ) {
- $this->generate_connection( $conn['node_type'] );
- }
-
- // Generate resolvers.
- foreach ( $queries as $fqcn => $ref ) {
- $this->generate_resolver( $fqcn, $ref, 'query' );
- }
- foreach ( $mutations as $fqcn => $ref ) {
- $this->generate_resolver( $fqcn, $ref, 'mutation' );
- }
-
- // Generate root types and type registry.
- $this->generate_root_query_type( $queries );
- $this->generate_root_mutation_type( $mutations );
- $this->generate_type_registry();
- $this->generate_graphql_controller();
- }
-
- private function discover_connections( array $queries, array $mutations ): void {
- foreach ( array_merge( $queries, $mutations ) as $fqcn => $ref ) {
- if ( ! $ref->hasMethod( 'execute' ) ) {
- continue;
- }
- $method = $ref->getMethod( 'execute' );
- $conn_attr = $method->getAttributes( ConnectionOf::class );
- if ( ! empty( $conn_attr ) ) {
- $node_type = $conn_attr[0]->newInstance()->type;
- $this->connections[ $node_type ] = array(
- 'node_type' => $node_type,
- 'source' => $ref->getShortName() . '::execute()',
- );
- }
- }
- }
-
- // ------ Enum ------
-
- private function generate_enum( string $fqcn, \ReflectionEnum $ref ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
- $description = $this->get_description( $ref );
- $enum_alias = $ref->getShortName() . 'Enum';
-
- $values = array();
- foreach ( $ref->getCases() as $case ) {
- $case_name_attr = $case->getAttributes( Name::class );
- $gql_case_name = ! empty( $case_name_attr )
- ? $case_name_attr[0]->newInstance()->name
- : $this->to_screaming_snake_case( $case->getName() );
-
- $deprecation = $case->getAttributes( Deprecated::class );
-
- $values[] = array(
- 'graphql_name' => $gql_case_name,
- 'case_name' => $case->getName(),
- 'description' => $this->get_description( $case ),
- 'deprecation_reason' => ! empty( $deprecation ) ? $deprecation[0]->newInstance()->reason : null,
- 'metadata' => $this->harvest_metadata( $case, "{$ref->getShortName()}::{$case->getName()}" ),
- );
- }
-
- $code = $this->render_template(
- 'EnumTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Enums',
- 'class_name' => $ref->getShortName(),
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'enum_fqcn' => $fqcn,
- 'enum_alias' => $enum_alias,
- 'values' => $values,
- 'metadata' => $this->harvest_metadata( $ref, $ref->getShortName() ),
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Enums/' . $ref->getShortName() . '.php';
- file_put_contents( $path, $code );
- ++$this->enum_count;
- }
-
- // ------ Scalar ------
-
- private function generate_scalar( string $fqcn, \ReflectionClass $ref ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
- $description = $this->get_description( $ref );
- $scalar_alias = $ref->getShortName() . 'Scalar';
-
- $code = $this->render_template(
- 'ScalarTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Scalars',
- 'class_name' => $ref->getShortName(),
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'scalar_fqcn' => $fqcn,
- 'scalar_alias' => $scalar_alias,
- 'metadata' => $this->harvest_metadata( $ref, $ref->getShortName() ),
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Scalars/' . $ref->getShortName() . '.php';
- file_put_contents( $path, $code );
- ++$this->scalar_count;
- }
-
- // ------ Interface ------
-
- private function generate_interface( string $fqcn, \ReflectionClass $ref ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
- $description = $this->get_description( $ref );
- $use_stmts = array();
- $fields = array();
- $type_level_usages = $this->collect_class_authorization_usages( $ref );
- $type_level_descriptors = $this->collect_class_authorization_descriptors( $ref );
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
-
- $field = $this->build_field_definition( $prop, 'output', $use_stmts, $type_level_usages, $type_level_descriptors );
- if ( $field !== null ) {
- $fields[] = $field;
- }
- }
-
- // Build resolveType map: PHP FQCN => generated ObjectType class alias.
- $type_map = array();
- foreach ( $this->interface_implementors[ $fqcn ] ?? array() as $impl_fqcn ) {
- $impl_short = ( new \ReflectionClass( $impl_fqcn ) )->getShortName();
- $alias = $impl_short . 'Type';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Output\\{$impl_short} as {$alias}";
- $type_map[] = array(
- 'fqcn' => $impl_fqcn,
- 'alias' => $alias,
- );
- }
-
- $type_metadata_visible = $this->is_target_metadata_visible( $ref );
- $code = $this->render_template(
- 'InterfaceTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Interfaces',
- 'class_name' => $ref->getShortName(),
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'use_statements' => array_unique( $use_stmts ),
- 'fields' => $fields,
- 'type_map' => $type_map,
- 'metadata' => $type_metadata_visible ? $this->harvest_metadata( $ref, $ref->getShortName() ) : array(),
- 'authorization' => $type_metadata_visible ? $this->collect_class_authorization_descriptors( $ref ) : array(),
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Interfaces/' . $ref->getShortName() . '.php';
- file_put_contents( $path, $code );
- ++$this->interface_count;
- }
-
- // ------ Output Type ------
-
- private function generate_output_type( string $fqcn, \ReflectionClass $ref ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
- $description = $this->get_description( $ref );
- $use_stmts = array();
- $interfaces = array();
- $fields = array();
- $type_level_usages = $this->collect_class_authorization_usages( $ref );
- $type_level_descriptors = $this->collect_class_authorization_descriptors( $ref );
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
-
- $field = $this->build_field_definition( $prop, 'output', $use_stmts, $type_level_usages, $type_level_descriptors );
- if ( $field !== null ) {
- $fields[] = $field;
- }
- }
-
- // Wire interfaces: check if any traits on this class are discovered interfaces.
- foreach ( $ref->getTraits() as $trait ) {
- $trait_fqcn = $trait->getName();
- $trait_info = $this->get_class_info( $trait_fqcn );
- if ( $trait_info !== null && $trait_info['kind'] === 'interface' ) {
- $iface_short = $trait->getShortName();
- $alias = $iface_short . 'Interface';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Interfaces\\{$iface_short} as {$alias}";
- $interfaces[] = array( 'alias' => $alias );
- }
- }
-
- $type_metadata_visible = $this->is_target_metadata_visible( $ref );
- $type_metadata_full = $this->harvest_metadata( $ref, $ref->getShortName() );
- $code = $this->render_template(
- 'ObjectTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Output',
- 'class_name' => $ref->getShortName(),
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'use_statements' => array_unique( $use_stmts ),
- 'interfaces' => $interfaces,
- 'fields' => $fields,
- // `metadata` feeds the `_apiMetadata` discovery channel and so respects
- // the per-target `shows_in_metadata_query()` opt-out; `metadata_runtime`
- // always carries the full set because field gates thread it into the
- // `$_metadata['type']` slice and that opt-out is discovery-only.
- 'metadata' => $type_metadata_visible ? $type_metadata_full : array(),
- 'metadata_runtime' => $type_metadata_full,
- 'authorization' => $type_metadata_visible ? $this->collect_class_authorization_descriptors( $ref ) : array(),
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Output/' . $ref->getShortName() . '.php';
- file_put_contents( $path, $code );
- ++$this->type_count;
- }
-
- // ------ Input Type ------
-
- private function generate_input_type( string $fqcn, \ReflectionClass $ref ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
-
- // Strip "Input" suffix for generated class name, but keep it for GraphQL name.
- $gen_class_name = $ref->getShortName();
- if ( str_ends_with( $gen_class_name, 'Input' ) ) {
- $gen_class_name = substr( $gen_class_name, 0, -5 );
- }
-
- $description = $this->get_description( $ref );
- $use_stmts = array();
- $fields = array();
- $type_level_usages = $this->collect_class_authorization_usages( $ref );
- $type_level_descriptors = $this->collect_class_authorization_descriptors( $ref );
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
-
- $field = $this->build_field_definition( $prop, 'input', $use_stmts, $type_level_usages, $type_level_descriptors );
- if ( $field !== null ) {
- $fields[] = $field;
- }
- }
-
- $type_metadata_visible = $this->is_target_metadata_visible( $ref );
- $code = $this->render_template(
- 'InputObjectTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Input',
- 'class_name' => $gen_class_name,
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'use_statements' => array_unique( $use_stmts ),
- 'fields' => $fields,
- 'metadata' => $type_metadata_visible ? $this->harvest_metadata( $ref, $ref->getShortName() ) : array(),
- 'authorization' => $type_metadata_visible ? $this->collect_class_authorization_descriptors( $ref ) : array(),
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Input/' . $gen_class_name . '.php';
- file_put_contents( $path, $code );
- ++$this->input_type_count;
- }
-
- // ------ Resolver ------
-
- private function generate_resolver( string $fqcn, \ReflectionClass $ref, string $kind ): void {
- $graphql_name = $this->graphql_names[ $fqcn ];
- $description = $this->get_description( $ref );
- $command_alias = $ref->getShortName() . 'Command';
- $use_stmts = array();
-
- $execute_method = $ref->getMethod( 'execute' );
- $params = $execute_method->getParameters();
-
- // Determine return type.
- $return_type = $execute_method->getReturnType();
- $connection_of = $execute_method->getAttributes( ConnectionOf::class );
- $has_connection_of = ! empty( $connection_of );
-
- $return_type_expr = $this->get_return_type_expr( $execute_method, $use_stmts );
-
- // Detect scalar return types (bool, int, float, string) to wrap in a result object.
- $return_type_name = $return_type instanceof \ReflectionNamedType ? $return_type->getName() : 'mixed';
- $scalar_return = in_array( $return_type_name, array( 'bool', 'int', 'float', 'string' ), true );
-
- // Build args and execute params.
- $args = array();
- $execute_params = array();
- $input_converters = array();
-
- // Track infrastructure params declared on execute() so the resolver
- // template can wire them in alongside _query_info. _preauthorized is
- // not allowed on execute() (it's an authorize-only signal); see
- // validate_infra_param().
- $execute_principal_arg = null;
- $execute_query_info_arg = false;
-
- foreach ( $params as $param ) {
- $param_name = $param->getName();
-
- // Infrastructure parameters: validated and emitted with is_infrastructure=true.
- $infra = $this->validate_infra_param( $param, false, $fqcn );
- if ( null !== $infra ) {
- $execute_params[] = array(
- 'name' => $param_name,
- 'conversion' => null,
- 'is_infrastructure' => true,
- );
- switch ( $param_name ) {
- case '_principal':
- $execute_principal_arg = array(
- 'type_name' => $infra['type_name'],
- );
- break;
- case '_query_info':
- $execute_query_info_arg = true;
- break;
- }
- continue;
- }
- // Skip params that started with `_` but were rejected — error is
- // already recorded by validate_infra_param().
- if ( '' !== $param_name && '_' === $param_name[0] ) {
- continue;
- }
-
- $param_type = $param->getType();
- $type_name = $param_type instanceof \ReflectionNamedType ? $param_type->getName() : 'mixed';
-
- // Unroll: expand each property of the class into a separate GraphQL arg.
- if ( $this->should_unroll( $param, $type_name ) ) {
- $unroll = $this->build_unroll_info( $type_name, $use_stmts );
- foreach ( $unroll['args'] as $uarg ) {
- $args[] = $uarg;
- }
- $execute_params[] = array(
- 'name' => $param_name,
- 'conversion' => null,
- 'is_infrastructure' => false,
- 'unroll' => $unroll,
- 'input_fqcn' => $unroll['fqcn'],
- );
- continue;
- }
-
- $arg_type_expr = $this->php_type_to_graphql_expr( $type_name, $param_type?->allowsNull() ?? false, $param, $use_stmts );
-
- $param_description = $this->get_param_description( $param );
-
- $arg_entry = array(
- 'name' => $param_name,
- 'type_expr' => $arg_type_expr,
- 'description' => $param_description,
- 'has_default' => $param->isDefaultValueAvailable(),
- 'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
- 'metadata' => $this->harvest_metadata( $param, "{$ref->getShortName()}::execute() parameter \${$param_name}" ),
- );
- $args[] = $arg_entry;
-
- // Determine conversion for execute params.
- $conversion = null;
- $input_info = $this->get_class_info( $type_name );
-
- if ( $input_info !== null && $input_info['kind'] === 'input_type' ) {
- // Input type: needs conversion.
- $converter_name = 'convert_' . $this->pascal_to_snake_case( ( new \ReflectionClass( $type_name ) )->getShortName() );
- $conversion = "self::{$converter_name}( \$args['{$param_name}'] )";
-
- // Build converter if not already done.
- if ( ! isset( $input_converters[ $type_name ] ) ) {
- $input_converters[ $type_name ] = $this->build_input_converter( $type_name, $input_converters );
- }
- } elseif ( $input_info !== null && $input_info['kind'] === 'enum' ) {
- // GraphQL engine already resolves enum input values to PHP enum instances,
- // so no ::from() conversion is needed — just assign the value directly.
- }
-
- $execute_params[] = array(
- 'name' => $param_name,
- 'conversion' => $conversion,
- 'is_infrastructure' => false,
- 'input_fqcn' => ( null !== $conversion && null !== $input_info && 'input_type' === $input_info['kind'] ) ? $type_name : null,
- );
- }
-
- // Authorization: check for authorize() method.
- $authorize_param_names = null;
- $has_preauthorized = false;
- $authorize_principal_arg = null;
- $authorize_query_info_arg = false;
- if ( $ref->hasMethod( 'authorize' ) ) {
- $authorize_method = $ref->getMethod( 'authorize' );
- $authorize_ignored = ! empty( $authorize_method->getAttributes( Ignore::class ) );
-
- if ( ! $authorize_ignored ) {
- $validated = $this->validate_authorize_method( $fqcn, $execute_method, $authorize_method );
- $authorize_param_names = $validated['domain_params'];
- $has_preauthorized = $validated['has_preauthorized'];
- $authorize_principal_arg = $validated['principal_arg'];
- $authorize_query_info_arg = $validated['query_info_arg'];
- }
- }
-
- // Collect input-side authorization gate descriptors. For every
- // execute() parameter whose runtime value is an input class instance
- // (either unrolled, or built by an input-converter), walk that class's
- // public properties for field-level authorization attributes. The
- // emitted gate fires only for *provided* fields so an unset/optional
- // property doesn't trigger a check, mirroring the
- // {@see \Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields}
- // convention that input types already follow.
- $input_side_gates = array();
- foreach ( $execute_params as $param ) {
- if ( empty( $param['input_fqcn'] ) || ! class_exists( $param['input_fqcn'] ) ) {
- continue;
- }
- $input_ref = new \ReflectionClass( $param['input_fqcn'] );
- $input_short = $input_ref->getShortName();
- $type_metadata_lit = self::render_php_literal( $this->harvest_metadata( $input_ref, $input_short ) );
- $input_type_usages = $this->collect_class_authorization_usages( $input_ref );
- $input_type_descriptors = $this->collect_class_authorization_descriptors( $input_ref );
- $field_descriptors = array();
- foreach ( $input_ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
- $field_auth = $this->resolve_field_authorization( $prop, $input_type_usages, $input_type_descriptors );
- if ( 'true' === $field_auth['attribute_expr'] ) {
- continue;
- }
- $field_metadata_lit = self::render_php_literal(
- $this->harvest_metadata( $prop, $input_short . '::$' . $prop->getName() )
- );
- $field_descriptors[] = array(
- 'field_name' => $prop->getName(),
- 'attribute_expr' => $field_auth['attribute_expr'],
- 'first_attribute_short' => $field_auth['first_attribute_short'],
- 'type_metadata_literal' => $type_metadata_lit,
- 'field_metadata_literal' => $field_metadata_lit,
- );
- }
- if ( ! empty( $field_descriptors ) ) {
- $input_side_gates[] = array(
- 'exec_arg_name' => $param['name'],
- 'input_fqcn' => $param['input_fqcn'],
- 'input_short_name' => $input_short,
- 'fields' => $field_descriptors,
- );
- }
- }
-
- // Resolve the attribute-declared authorization. The `attribute_expr` is
- // the AND of all autodiscovered authorization attributes' authorize()
- // calls (or `'true'` when the only attribute is PublicAccess, or when
- // there are no attributes — in which case the command's own authorize()
- // is the sole guard). The expression goes into a generated static
- // `compute_preauthorized()` helper that's the single source of truth
- // for both the resolver's own gates and external (code-API) callers
- // asking "would `_preauthorized` be true for this caller?".
- $auth = $this->resolve_authorization( $ref );
- $attribute_expr = $auth['attribute_expr'];
- $has_attribute_expr = ! empty( $auth['usages'] );
- $has_authorize = null !== $authorize_param_names;
- // Skip the standalone gate when the attribute expression is trivially `true`
- // (PublicAccess) — emitting `if (! (true))` is dead code that the linter would
- // flag.
- $standalone_attribute_check = $has_attribute_expr && ! $has_authorize && 'true' !== $attribute_expr;
-
- // `_preauthorized` is only meaningful when authorize() declares the
- // $_preauthorized parameter; otherwise the variable is unused and stays
- // at 'false'. When the authorize() method isn't declared at all, the
- // expression gets emitted directly as a standalone gate (see
- // `standalone_attribute_check`).
- $preauthorized_expr = $has_preauthorized ? "self::compute_preauthorized( \$context['principal'] )" : 'false';
-
- // Signature for the public compute_preauthorized() helper. `<PrincipalType>`
- // when a PrincipalResolver was detected, else `object` (catch-all when no
- // resolver constrains the runtime type). Always non-null — anonymous is
- // represented by a sentinel principal, not by null.
- $compute_preauthorized_param_type = 'object' === $this->principal_type
- ? 'object'
- : '\\' . ltrim( $this->principal_type, '\\' );
-
- $dir_name = $kind === 'query' ? 'GraphQLQueries' : 'GraphQLMutations';
- $namespace = $this->autogenerated_namespace . '\\' . $dir_name;
-
- $code = $this->render_template(
- 'QueryResolverTemplate.php',
- array(
- 'namespace' => $namespace,
- 'class_name' => $ref->getShortName(),
- 'graphql_name' => $graphql_name,
- 'description' => $description,
- 'command_fqcn' => $fqcn,
- 'command_alias' => $command_alias,
- 'class_resolver_fqcn' => $this->class_resolver_fqcn,
- 'return_type_expr' => $return_type_expr,
- 'use_statements' => array_unique( $use_stmts ),
- 'args' => $args,
- 'has_connection_of' => $has_connection_of,
- 'connection_type_alias' => '',
- 'execute_params' => $execute_params,
- 'execute_principal_arg' => $execute_principal_arg,
- 'execute_query_info_arg' => $execute_query_info_arg,
- 'input_converters' => array_values( $input_converters ),
- 'authorize_param_names' => $authorize_param_names,
- 'has_preauthorized' => $has_preauthorized,
- 'preauthorized_expr' => $preauthorized_expr,
- 'authorize_principal_arg' => $authorize_principal_arg,
- 'authorize_query_info_arg' => $authorize_query_info_arg,
- 'standalone_attribute_check' => $standalone_attribute_check,
- 'attribute_expr' => $attribute_expr,
- 'compute_preauthorized_param_type' => $compute_preauthorized_param_type,
- 'scalar_return' => $scalar_return,
- // `metadata` feeds the operation's `_apiMetadata` row (discovery, so it
- // honours `shows_in_metadata_query()`); `metadata_runtime` is published
- // into `$context['_query_metadata']` for downstream field gates and so
- // always carries the full set (the opt-out is discovery-only).
- 'metadata' => $this->is_target_metadata_visible( $ref ) ? $this->harvest_metadata( $ref, $ref->getShortName() ) : array(),
- 'metadata_runtime' => $this->harvest_metadata( $ref, $ref->getShortName() ),
- 'input_side_gates' => $input_side_gates,
- 'authorization_descriptors' => $this->is_target_metadata_visible( $ref ) ? $auth['descriptors'] : array(),
- )
- );
-
- $path = $this->autogenerated_dir . "/{$dir_name}/" . $ref->getShortName() . '.php';
- file_put_contents( $path, $code );
-
- if ( $kind === 'query' ) {
- ++$this->query_count;
- } else {
- ++$this->mutation_count;
- }
- }
-
- /**
- * Recognised infrastructure parameter names (allowed on authorize() / execute() / attribute-authorize()).
- *
- * - `_query_info`: parsed selection tree, populated from the resolver $info argument.
- * - `_preauthorized`: bool result of attribute-declared authorization, populated by the resolver.
- * - `_principal`: per-request principal resolved by the configured PrincipalResolver.
- *
- * The principal is the only channel through which request-derived data
- * reaches commands; anything the resolver wants to expose to authorize() /
- * execute() (custom headers, request id, locale, etc.) goes onto the
- * principal class. This keeps commands network-agnostic and unit-testable.
- */
- private const INFRA_PARAM_NAMES = array( '_query_info', '_preauthorized', '_principal' );
-
- /**
- * Validate one infrastructure parameter on authorize() / execute().
- *
- * Returns null when the parameter isn't infra (caller treats it as a domain
- * parameter). Returns an info array describing the valid infra param when
- * recognised. Records an error and returns null when the parameter is
- * misshapen (wrong type, or unknown `_`-prefixed name) — letting the build
- * fail loudly rather than silently accepting a typoed `$_priincipal` as a
- * GraphQL arg.
- *
- * @param \ReflectionParameter $param The parameter to inspect.
- * @param bool $on_authorize True when validating authorize(); false for execute().
- * @param string $fqcn Owning class FQCN, for error messages.
- *
- * @return ?array{name: string, type_name: string}
- */
- private function validate_infra_param( \ReflectionParameter $param, bool $on_authorize, string $fqcn ): ?array {
- $name = $param->getName();
- if ( '' === $name || '_' !== $name[0] ) {
- return null;
- }
-
- $type = $param->getType();
- $type_name = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
- $nullable = null !== $type && $type->allowsNull();
- $method = $on_authorize ? 'authorize' : 'execute';
- $info = array(
- 'name' => $name,
- 'type_name' => $type_name,
- );
-
- switch ( $name ) {
- case '_query_info':
- return $info;
-
- case '_preauthorized':
- if ( ! $on_authorize ) {
- $this->errors[] = "{$fqcn}: \$_preauthorized is only allowed on authorize().";
- return null;
- }
- if ( 'bool' !== $type_name ) {
- $this->errors[] = "{$fqcn}: {$method}() parameter \$_preauthorized must be typed as bool.";
- return null;
- }
- return $info;
-
- case '_principal':
- if ( $nullable ) {
- $this->errors[] = "{$fqcn}: {$method}() parameter \$_principal must be non-nullable (anonymous requests are represented by a sentinel principal, not null); got `?{$type_name}`.";
- return null;
- }
- if ( ! $this->is_principal_type_compatible( $type_name ) ) {
- $expected = 'object' === $this->principal_type
- ? '`object` (or any object class)'
- : "`{$this->principal_type}` or a supertype, or `object`";
- $this->errors[] = "{$fqcn}: {$method}() parameter \$_principal must be typed as {$expected}; got `{$type_name}`.";
- return null;
- }
- return $info;
-
- default:
- $allowed = '$' . implode( ', $', self::INFRA_PARAM_NAMES );
- $this->errors[] = "{$fqcn}: {$method}() parameter \${$name} starts with `_` but is not a recognised infrastructure parameter (allowed: {$allowed}).";
- return null;
- }
- }
-
- /**
- * Whether a declared `_principal` type is compatible with the registered principal type.
- *
- * Compatibility rules:
- * - `object` (the declared parameter type) is always accepted (catch-all supertype).
- * - When the registered principal type is `object` (the plugin's resolver explicitly
- * returned `object`), any object-like class name is accepted; the runtime type is
- * the plugin's responsibility.
- * - Otherwise the declared type must equal the registered principal type. Inheritance-
- * based supertype matching isn't enforced at build time because the principal class
- * (e.g. WP_User) typically isn't autoloadable in the build environment; users that
- * want supertype polymorphism should declare `object` and narrow at runtime.
- *
- * @param string $declared_type Type name from the parameter's ReflectionNamedType (no leading backslash).
- */
- private function is_principal_type_compatible( string $declared_type ): bool {
- $declared_type = ltrim( $declared_type, '\\' );
- if ( 'object' === $declared_type ) {
- return true;
- }
-
- $rejected_scalars = array( 'void', 'never', 'null', 'mixed', 'string', 'int', 'float', 'bool', 'array', 'iterable', 'callable', 'self', 'static', 'parent' );
- if ( in_array( $declared_type, $rejected_scalars, true ) ) {
- return false;
- }
-
- // No resolver registered → any object-like type is acceptable.
- if ( 'object' === $this->principal_type ) {
- return true;
- }
-
- return $declared_type === ltrim( $this->principal_type, '\\' );
- }
-
- /**
- * Validate that an authorize() method's parameters are a subset of execute()'s domain
- * parameters, plus a recognised set of infrastructure parameters.
- *
- * Domain parameters on authorize() must appear on execute() with the same type.
- * Infrastructure parameters are validated independently — see
- * {@see self::validate_infra_param()} for the recognised set and the rules.
- *
- * @return array{
- * domain_params: string[],
- * has_preauthorized: bool,
- * principal_arg: ?array{type_name: string},
- * query_info_arg: bool,
- * }
- */
- private function validate_authorize_method(
- string $fqcn,
- \ReflectionMethod $execute_method,
- \ReflectionMethod $authorize_method,
- ): array {
- $execute_params = array();
- foreach ( $execute_method->getParameters() as $p ) {
- if ( in_array( $p->getName(), self::INFRA_PARAM_NAMES, true ) ) {
- continue;
- }
- $type = $p->getType();
- $execute_params[ $p->getName() ] = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
- }
-
- $domain_params = array();
- $has_preauthorized = false;
- $principal_arg = null;
- $query_info_arg = false;
-
- foreach ( $authorize_method->getParameters() as $p ) {
- $infra = $this->validate_infra_param( $p, true, $fqcn );
- if ( null !== $infra ) {
- switch ( $infra['name'] ) {
- case '_preauthorized':
- $has_preauthorized = true;
- break;
- case '_principal':
- $principal_arg = array(
- 'type_name' => $infra['type_name'],
- );
- break;
- case '_query_info':
- $query_info_arg = true;
- break;
- }
- continue;
- }
- // Skip params that started with `_` but were rejected — error is
- // already recorded by validate_infra_param().
- if ( '' !== $p->getName() && '_' === $p->getName()[0] ) {
- continue;
- }
-
- $name = $p->getName();
- $type = $p->getType();
- $type_name = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
-
- if ( ! array_key_exists( $name, $execute_params ) ) {
- $this->errors[] = "{$fqcn}: authorize() parameter \${$name} does not exist in execute().";
- continue;
- }
-
- if ( $execute_params[ $name ] !== $type_name ) {
- $this->errors[] = "{$fqcn}: authorize() parameter \${$name} has type {$type_name}, but execute() has {$execute_params[$name]}.";
- continue;
- }
-
- $domain_params[] = $name;
- }
-
- return array(
- 'domain_params' => $domain_params,
- 'has_preauthorized' => $has_preauthorized,
- 'principal_arg' => $principal_arg,
- 'query_info_arg' => $query_info_arg,
- );
- }
-
- /**
- * Whether a parameter should be unrolled into flat GraphQL args.
- */
- private function should_unroll( \ReflectionParameter $param, string $type_name ): bool {
- // Attribute on the parameter itself.
- if ( ! empty( $param->getAttributes( Unroll::class ) ) ) {
- return true;
- }
-
- // Attribute on the class.
- if ( class_exists( $type_name ) ) {
- $ref = new \ReflectionClass( $type_name );
- if ( ! empty( $ref->getAttributes( Unroll::class ) ) ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Whether a Parameter attribute should be unrolled into flat GraphQL args.
- */
- private function should_unroll_parameter( Parameter $param ): bool {
- if ( $param->unroll ) {
- return true;
- }
-
- if ( class_exists( $param->type ) ) {
- $ref = new \ReflectionClass( $param->type );
- if ( ! empty( $ref->getAttributes( Unroll::class ) ) ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Build unroll metadata: the list of GraphQL args and constructor properties
- * derived from the public properties of the given class.
- */
- private function build_unroll_info( string $fqcn, array &$use_stmts ): array {
- $ref = new \ReflectionClass( $fqcn );
- $args = array();
- $properties = array();
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
-
- $prop_name = $prop->getName();
- $type = $prop->getType();
- $type_name = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
- $nullable = $type?->allowsNull() ?? false;
-
- // Description from attribute.
- $desc_attrs = $prop->getAttributes( Description::class );
- $description = ! empty( $desc_attrs ) ? $desc_attrs[0]->newInstance()->description : '';
- $description = $this->apply_description_transforms( $description, $prop );
-
- // Default value.
- $has_default = $prop->hasDefaultValue();
- $default = $has_default ? $prop->getDefaultValue() : null;
-
- // If promoted, the default may come from the constructor parameter.
- if ( ! $has_default && $prop->isPromoted() ) {
- $ctor = $ref->getConstructor();
- if ( $ctor !== null ) {
- foreach ( $ctor->getParameters() as $ctor_param ) {
- if ( $ctor_param->getName() === $prop_name && $ctor_param->isDefaultValueAvailable() ) {
- $has_default = true;
- $default = $ctor_param->getDefaultValue();
- break;
- }
- }
- }
- }
-
- // GraphQL type expression.
- $type_expr = $this->php_type_to_graphql_expr( $type_name, $nullable, $prop, $use_stmts );
-
- $args[] = array(
- 'name' => $prop_name,
- 'type_expr' => $type_expr,
- 'description' => $description,
- 'has_default' => $has_default,
- 'default' => $default,
- // Unrolled args inherit metadata from the source property: the
- // unroll is a renaming of input fields into top-level args, so
- // any `#[Metadata]` on the property travels with the rename.
- // Use the declaring class for the label so the cache populated
- // by `validate_metadata()`'s property walk (which keys by the
- // declaring class) hits the same entry and conflicts are only
- // recorded once.
- 'metadata' => $this->harvest_metadata( $prop, "{$prop->getDeclaringClass()->getShortName()}::\${$prop_name}" ),
- );
-
- // Value expression for the constructor call.
- $class_info = $this->get_class_info( $type_name );
- if ( $class_info !== null && $class_info['kind'] === 'enum' ) {
- // GraphQL engine already resolves enum input values to PHP enum instances,
- // so no ::from() conversion is needed — just assign the value directly.
- $value_expr = "\$args['{$prop_name}']";
- } else {
- $value_expr = "\$args['{$prop_name}']";
- if ( $has_default ) {
- $value_expr .= ' ?? ' . var_export( $default, true );
- }
- }
-
- $properties[] = array(
- 'name' => $prop_name,
- 'value_expr' => $value_expr,
- );
- }
-
- return array(
- 'fqcn' => $fqcn,
- 'args' => $args,
- 'properties' => $properties,
- );
- }
-
- private function build_input_converter( string $input_fqcn, array &$input_converters ): array {
- $ref = new \ReflectionClass( $input_fqcn );
- $method_name = 'convert_' . $this->pascal_to_snake_case( $ref->getShortName() );
- $properties = array();
-
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- $type = $prop->getType();
- $type_name = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
-
- $conversion = null;
- $class_info = $this->get_class_info( $type_name );
-
- if ( $class_info !== null && $class_info['kind'] === 'enum' ) {
- // GraphQL engine already resolves enum input values to PHP enum instances,
- // so no ::from() conversion is needed — just assign the value directly.
- } elseif ( $class_info !== null && $class_info['kind'] === 'input_type' ) {
- $nested_short = ( new \ReflectionClass( $type_name ) )->getShortName();
- $nested_method = 'convert_' . $this->pascal_to_snake_case( $nested_short );
- $prop_name = $prop->getName();
-
- if ( $type->allowsNull() ) {
- $conversion = "null !== \$data['{$prop_name}'] ? self::{$nested_method}( \$data['{$prop_name}'] ) : null";
- } else {
- $conversion = "self::{$nested_method}( \$data['{$prop_name}'] )";
- }
-
- // Recursively build the nested converter if not already registered.
- if ( ! isset( $input_converters[ $type_name ] ) ) {
- $input_converters[ $type_name ] = $this->build_input_converter( $type_name, $input_converters );
- }
- }
-
- $properties[] = array(
- 'name' => $prop->getName(),
- 'conversion' => $conversion,
- );
- }
-
- return array(
- 'method_name' => $method_name,
- 'input_fqcn' => $input_fqcn,
- 'input_class' => $ref->getShortName(),
- 'properties' => $properties,
- );
- }
-
- // ------ Connection ------
-
- private function generate_connection( string $node_type_fqcn ): void {
- $node_ref = $this->classes[ $node_type_fqcn ]['class'] ?? new \ReflectionClass( $node_type_fqcn );
- $node_name = $node_ref->getShortName();
-
- $namespace = $this->autogenerated_namespace . '\\GraphQLTypes\\Pagination';
- $node_type_class = $node_name;
- $node_info = $this->get_class_info( $node_type_fqcn );
- $node_type_namespace = ( null !== $node_info && 'interface' === $node_info['kind'] )
- ? $this->autogenerated_namespace . '\\GraphQLTypes\\Interfaces'
- : $this->autogenerated_namespace . '\\GraphQLTypes\\Output';
- $node_type_alias = $node_name . 'Type';
- $connection_class_name = $node_name . 'Connection';
- $edge_class_name = $node_name . 'Edge';
-
- $connection_code = $this->generate_connection_code( $namespace, $node_type_class, $node_type_namespace, $node_type_alias, $connection_class_name, $edge_class_name );
- $edge_code = $this->generate_edge_code( $namespace, $node_type_class, $node_type_namespace, $node_type_alias, $edge_class_name );
-
- $connection_path = $this->autogenerated_dir . '/GraphQLTypes/Pagination/' . $connection_class_name . '.php';
- file_put_contents( $connection_path, $connection_code );
-
- $edge_path = $this->autogenerated_dir . '/GraphQLTypes/Pagination/' . $edge_class_name . '.php';
- file_put_contents( $edge_path, $edge_code );
- }
-
- private function generate_connection_code( string $namespace, string $node_type_class, string $node_type_namespace, string $node_type_alias, string $connection_class_name, string $edge_class_name ): string {
- $code = "<?php\n\n";
- $code .= "declare(strict_types=1);\n\n";
- $code .= "// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.\n\n";
- $code .= "namespace {$namespace};\n\n";
- $code .= "use {$node_type_namespace}\\{$node_type_class} as {$node_type_alias};\n";
- $code .= "use Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\ObjectType;\n";
- $code .= "use Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\Type;\n\n";
- $code .= "class {$connection_class_name} {\n";
- $code .= "\tprivate static ?ObjectType \$instance = null;\n\n";
- $code .= "\tpublic static function get(): ObjectType {\n";
- $code .= "\t\tif ( null === self::\$instance ) {\n";
- $code .= "\t\t\tself::\$instance = new ObjectType(\n";
- $code .= "\t\t\t\tarray(\n";
- $code .= "\t\t\t\t\t'name' => '{$connection_class_name}',\n";
- $code .= "\t\t\t\t\t'description' => __( 'A connection to a list of {$node_type_class} items.', 'woocommerce' ),\n";
- $code .= "\t\t\t\t\t'fields' => fn() => array(\n";
- $code .= "\t\t\t\t\t\t'edges' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( Type::listOf( Type::nonNull(\n";
- $code .= "\t\t\t\t\t\t\t\t{$edge_class_name}::get()\n";
- $code .= "\t\t\t\t\t\t\t) ) ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t\t'nodes' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( Type::listOf( Type::nonNull(\n";
- $code .= "\t\t\t\t\t\t\t\t{$node_type_alias}::get()\n";
- $code .= "\t\t\t\t\t\t\t) ) ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t\t'page_info' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( PageInfo::get() ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t\t'total_count' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( Type::int() ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t),\n";
- $code .= "\t\t\t\t)\n";
- $code .= "\t\t\t);\n";
- $code .= "\t\t}\n";
- $code .= "\t\treturn self::\$instance;\n";
- $code .= "\t}\n";
- $code .= "}\n";
-
- return $code;
- }
-
- private function generate_edge_code( string $namespace, string $node_type_class, string $node_type_namespace, string $node_type_alias, string $edge_class_name ): string {
- $code = "<?php\n\n";
- $code .= "declare(strict_types=1);\n\n";
- $code .= "// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.\n\n";
- $code .= "namespace {$namespace};\n\n";
- $code .= "use {$node_type_namespace}\\{$node_type_class} as {$node_type_alias};\n";
- $code .= "use Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\ObjectType;\n";
- $code .= "use Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\Type;\n\n";
- $code .= "class {$edge_class_name} {\n";
- $code .= "\tprivate static ?ObjectType \$instance = null;\n\n";
- $code .= "\tpublic static function get(): ObjectType {\n";
- $code .= "\t\tif ( null === self::\$instance ) {\n";
- $code .= "\t\t\tself::\$instance = new ObjectType(\n";
- $code .= "\t\t\t\tarray(\n";
- $code .= "\t\t\t\t\t'name' => '{$edge_class_name}',\n";
- $code .= "\t\t\t\t\t'fields' => fn() => array(\n";
- $code .= "\t\t\t\t\t\t'cursor' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( Type::string() ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t\t'node' => array(\n";
- $code .= "\t\t\t\t\t\t\t'type' => Type::nonNull( {$node_type_alias}::get() ),\n";
- $code .= "\t\t\t\t\t\t),\n";
- $code .= "\t\t\t\t\t),\n";
- $code .= "\t\t\t\t)\n";
- $code .= "\t\t\t);\n";
- $code .= "\t\t}\n";
- $code .= "\t\treturn self::\$instance;\n";
- $code .= "\t}\n";
- $code .= "}\n";
-
- return $code;
- }
-
- private function generate_page_info(): void {
- $code = $this->render_template(
- 'PageInfoTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace . '\\GraphQLTypes\\Pagination',
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLTypes/Pagination/PageInfo.php';
- file_put_contents( $path, $code );
- }
-
- // ------ Root Types ------
-
- private function generate_root_query_type( array $queries ): void {
- $query_data = array();
- foreach ( $queries as $fqcn => $ref ) {
- $query_data[] = array(
- 'class_name' => $ref->getShortName(),
- 'fqcn' => $this->autogenerated_namespace . '\\GraphQLQueries\\' . $ref->getShortName(),
- 'graphql_name' => $this->root_field_name( $fqcn, $ref ),
- );
- }
-
- $code = $this->render_template(
- 'RootQueryTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace,
- 'queries' => $query_data,
- )
- );
-
- $path = $this->autogenerated_dir . '/RootQueryType.php';
- file_put_contents( $path, $code );
- }
-
- private function generate_root_mutation_type( array $mutations ): void {
- $mutation_data = array();
- foreach ( $mutations as $fqcn => $ref ) {
- $mutation_data[] = array(
- 'class_name' => $ref->getShortName(),
- 'fqcn' => $this->autogenerated_namespace . '\\GraphQLMutations\\' . $ref->getShortName(),
- 'graphql_name' => $this->root_field_name( $fqcn, $ref ),
- );
- }
-
- $code = $this->render_template(
- 'RootMutationTypeTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace,
- 'mutations' => $mutation_data,
- )
- );
-
- $path = $this->autogenerated_dir . '/RootMutationType.php';
- file_put_contents( $path, $code );
- }
-
- private function generate_type_registry(): void {
- $types = array();
- foreach ( $this->interface_implementors as $implementors ) {
- foreach ( $implementors as $impl_fqcn ) {
- $short = ( new \ReflectionClass( $impl_fqcn ) )->getShortName();
- $types[] = array(
- 'short_name' => $short,
- 'fqcn' => $this->autogenerated_namespace . "\\GraphQLTypes\\Output\\{$short}",
- );
- }
- }
-
- $code = $this->render_template(
- 'TypeRegistryTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace,
- 'types' => $types,
- )
- );
-
- $path = $this->autogenerated_dir . '/TypeRegistry.php';
- file_put_contents( $path, $code );
- }
-
- private function generate_graphql_controller(): void {
- $code = $this->render_template(
- 'GraphQLControllerTemplate.php',
- array(
- 'namespace' => $this->autogenerated_namespace,
- 'class_resolver_fqcn' => $this->class_resolver_fqcn,
- 'principal_resolver_fqcn' => $this->principal_resolver_fqcn,
- 'principal_resolver_takes_request' => $this->principal_resolver_takes_request,
- 'status_resolver_fqcn' => $this->status_resolver_fqcn,
- )
- );
-
- $path = $this->autogenerated_dir . '/GraphQLController.php';
- file_put_contents( $path, $code );
- }
-
- // ========================================================================
- // Helpers
- // ========================================================================
-
- private function write_timestamp(): void {
- file_put_contents(
- $this->autogenerated_dir . '/api_generation_date.txt',
- gmdate( 'c' )
- );
- file_put_contents(
- $this->autogenerated_dir . '/api_source_hash.txt',
- StalenessChecker::compute_source_hash( $this->api_dir )
- );
- }
-
- private function render_template( string $template_name, array $vars ): string {
- extract( $vars );
- ob_start();
- require self::TEMPLATES_DIR . '/' . $template_name;
- return ob_get_clean();
- }
-
- private function get_description( \ReflectionClass|\ReflectionEnum|\ReflectionEnumUnitCase $ref ): string {
- $attrs = $ref->getAttributes( Description::class );
- $description = ! empty( $attrs ) ? $attrs[0]->newInstance()->description : '';
- return $this->apply_description_transforms( $description, $ref );
- }
-
- private function get_param_description( \ReflectionParameter $param ): string {
- $attrs = $param->getAttributes( Description::class );
- $description = ! empty( $attrs ) ? $attrs[0]->newInstance()->description : '';
- return $this->apply_description_transforms( $description, $param );
- }
-
- /**
- * Thread the description through {@see Metadata::transform_description()}
- * on every `Metadata`-derived attribute applied to the element.
- *
- * The base `Metadata` implementation is a no-op; subclasses opt into the
- * description-mirror convention by overriding (`#[Internal]` prefixes
- * `[Internal] ` and supplies a default body, `#[Experimental]` does the
- * analogous thing, etc.). Calls happen in PHP reflection (source) order,
- * threading each return value into the next, so the last attribute in
- * source ends up as the outermost prefix.
- *
- * The returned text is consumed verbatim by the templates and wrapped in
- * `__( ..., 'woocommerce' )`, so any default description supplied by a
- * subclass flows through the usual translation pipeline like any other
- * description.
- *
- * @param string $description Original description text (empty when the element has no `#[Description]`).
- * @param \ReflectionClass|\ReflectionEnum|\ReflectionEnumUnitCase|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $source Reflector whose `Metadata`-derived attributes drive the transforms.
- */
- private function apply_description_transforms( string $description, $source ): string {
- foreach ( $source->getAttributes( Metadata::class, \ReflectionAttribute::IS_INSTANCEOF ) as $attribute ) {
- $description = $attribute->newInstance()->transform_description( $description );
- }
- return $description;
- }
-
- /**
- * Walk every discovered class and emit a build-time warning for any
- * attribute reference whose class doesn't autoload.
- *
- * Symptom this catches: a developer writes `#[Metadata( … )]` (or any
- * other attribute) without importing the class, so PHP resolves the
- * reference relative to the current namespace and lands on a non-existent
- * class. ApiBuilder filters attributes by class via reflection and
- * silently ignores unresolved ones, so the attribute disappears from the
- * generated tree without any visible error — confusing to debug from the
- * runtime side. The warning surfaces the typo / missing import at build
- * time instead.
- *
- * The check is intentionally untyped: it warns about every attribute it
- * can't resolve, regardless of which `Metadata`/`Description`/`Internal`/etc.
- * the developer intended. The warning text quotes the unresolved FQCN so
- * the developer can see exactly what PHP tried to load.
- */
- private function check_attribute_resolutions(): void {
- foreach ( $this->classes as $info ) {
- $ref = $info['class'];
- $this->check_attributes_on( $ref, $ref->getShortName() );
-
- foreach ( $ref->getProperties() as $prop ) {
- $this->check_attributes_on( $prop, $ref->getShortName() . '::$' . $prop->getName() );
- }
-
- foreach ( $ref->getMethods() as $method ) {
- if ( $method->getDeclaringClass()->getName() !== $ref->getName() ) {
- continue;
- }
- $this->check_attributes_on( $method, $ref->getShortName() . '::' . $method->getName() . '()' );
-
- foreach ( $method->getParameters() as $param ) {
- $this->check_attributes_on(
- $param,
- $ref->getShortName() . '::' . $method->getName() . '($' . $param->getName() . ')'
- );
- }
- }
-
- if ( $ref instanceof \ReflectionEnum ) {
- foreach ( $ref->getCases() as $case ) {
- $this->check_attributes_on( $case, $ref->getShortName() . '::' . $case->getName() );
- }
- }
- }
- }
-
- /**
- * Check every attribute applied to a single reflector and accumulate a
- * warning per unresolvable class.
- *
- * @param \ReflectionClass|\ReflectionEnum|\ReflectionEnumUnitCase|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $source Reflector to scan.
- * @param string $context_label Human-readable label used in the warning.
- */
- private function check_attributes_on( $source, string $context_label ): void {
- foreach ( $source->getAttributes() as $attribute ) {
- $name = $attribute->getName();
- if ( ! class_exists( $name ) ) {
- $this->warnings[] = "{$context_label}: attribute `{$name}` could not be resolved (likely a missing `use` statement). The attribute was silently ignored during generation.";
- }
- }
- }
-
- /**
- * Collect {@see Metadata}-derived attributes from a reflector into a
- * `name => value` map.
- *
- * Two attributes producing the same `name` on the same element are a
- * generation-time error: surprising metadata in production is worse than a
- * loud build failure. The check spans subclasses too (e.g. a future
- * `#[Beta]` that also yielded `name = 'internal'` would conflict with a
- * sibling `#[Internal]`), which is why we match by `instanceof Metadata`
- * rather than by attribute class.
- *
- * @param \ReflectionClass|\ReflectionEnum|\ReflectionEnumUnitCase|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $source Reflector to read attributes from.
- * @param string $context_label Human-readable label for the source, used in error messages (e.g. `"Coupon::$lock_state"`).
- *
- * @return array<string, bool|int|float|string|null>
- */
- private function harvest_metadata( $source, string $context_label ): array {
- if ( array_key_exists( $context_label, $this->metadata_cache ) ) {
- return $this->metadata_cache[ $context_label ];
- }
-
- $entries = array();
- foreach ( $source->getAttributes( Metadata::class, \ReflectionAttribute::IS_INSTANCEOF ) as $attribute ) {
- $instance = $attribute->newInstance();
- $name = $instance->get_name();
- if ( array_key_exists( $name, $entries ) ) {
- $this->errors[] = "{$context_label}: duplicate metadata name '{$name}'.";
- continue;
- }
- $entries[ $name ] = $instance->get_value();
- }
- $this->metadata_cache[ $context_label ] = $entries;
- return $entries;
- }
-
- /**
- * Pre-generate pass that walks every reflector the generate phase will
- * later harvest metadata from, populating {@see self::$metadata_cache}
- * and recording duplicate-name conflicts in {@see self::$errors}.
- *
- * Without this pass, conflicts only surface inside `generate_*` — which
- * runs after {@see self::wipe_autogenerated()} — so a bad attribute set
- * would silently rewrite the autogenerated tree (keeping the first-seen
- * value) and the error would be lost because nothing reads `$errors`
- * after generate(). Running the walk up front lets the existing
- * build-level error check catch duplicates before any file is touched.
- */
- private function validate_metadata(): void {
- foreach ( $this->classes as $info ) {
- if ( $info['ignored'] || 'pagination' === $info['kind'] ) {
- continue;
- }
-
- $ref = $info['class'];
- $kind = $info['kind'];
-
- // Type-level metadata on every kind that emits a webonyx type config.
- if ( in_array( $kind, array( 'type', 'input_type', 'enum', 'interface', 'scalar', 'query', 'mutation' ), true ) ) {
- $this->harvest_metadata( $ref, $ref->getShortName() );
- }
-
- // Per-property metadata on output/input/interface types. The label must
- // match `build_field_definition()`'s, which uses the property's
- // declaring class — that handles inherited public properties cleanly.
- if ( in_array( $kind, array( 'type', 'input_type', 'interface' ), true ) ) {
- foreach ( $ref->getProperties( \ReflectionProperty::IS_PUBLIC ) as $prop ) {
- if ( ! empty( $prop->getAttributes( Ignore::class ) ) ) {
- continue;
- }
- $label = $prop->getDeclaringClass()->getShortName() . '::$' . $prop->getName();
- $this->harvest_metadata( $prop, $label );
- }
- }
-
- // Enum cases.
- if ( 'enum' === $kind && $ref instanceof \ReflectionEnum ) {
- foreach ( $ref->getCases() as $case ) {
- $this->harvest_metadata( $case, "{$ref->getShortName()}::{$case->getName()}" );
- }
- }
-
- // Query/mutation execute() parameters. Skip infrastructure parameters
- // (`_principal`, `_query_info`, `_preauthorized`) — they have no
- // matching schema element and never carry `#[Metadata]`. Unrolled
- // parameters reach harvest_metadata through their backing input type's
- // properties, which the property walk above already covers.
- if ( in_array( $kind, array( 'query', 'mutation' ), true ) && $ref->hasMethod( 'execute' ) ) {
- foreach ( $ref->getMethod( 'execute' )->getParameters() as $param ) {
- $param_name = $param->getName();
- if ( '' !== $param_name && '_' === $param_name[0] ) {
- continue;
- }
- $this->harvest_metadata( $param, "{$ref->getShortName()}::execute() parameter \${$param_name}" );
- }
- }
- }
- }
-
- private function get_class_info( string $class_name ): ?array {
- return $this->classes[ $class_name ] ?? null;
- }
-
- /**
- * Build the field-definition array consumed by templates.
- *
- * Per-field metadata is harvested from the property itself. Per-argument
- * metadata for `#[Parameter]`-declared arguments on output fields is *not*
- * supported in the MVP because the `Parameter` attribute carries the arg
- * shape inline; there is no separate target to decorate with `#[Metadata]`.
- * Root-operation arguments (declared as `execute()` parameters) do get
- * per-argument metadata — that path runs through {@see self::generate_resolver()}.
- */
- private function build_field_definition( \ReflectionProperty $prop, string $context, array &$use_stmts, array $type_level_usages = array(), array $type_level_descriptors = array() ): ?array {
- $type = $prop->getType();
- $type_name = $type instanceof \ReflectionNamedType ? $type->getName() : 'mixed';
- $nullable = $type?->allowsNull() ?? false;
- $has_default = $prop->hasDefaultValue();
-
- // For input types, nullable or has-default fields are optional (no nonNull wrapper).
- $is_optional = $context === 'input' && ( $nullable || $has_default );
-
- $type_expr = $this->php_type_to_graphql_expr( $type_name, $nullable || $is_optional, $prop, $use_stmts );
-
- $description = '';
- $desc_attrs = $prop->getAttributes( Description::class );
- if ( ! empty( $desc_attrs ) ) {
- $description = $desc_attrs[0]->newInstance()->description;
- }
- $description = $this->apply_description_transforms( $description, $prop );
-
- $deprecation = $prop->getAttributes( Deprecated::class );
-
- // Field arguments (only for output types).
- $args = array();
- if ( $context === 'output' ) {
- $param_attrs = $prop->getAttributes( Parameter::class );
- foreach ( $param_attrs as $pa ) {
- $param_inst = $pa->newInstance();
-
- if ( $this->should_unroll_parameter( $param_inst ) ) {
- $unroll_info = $this->build_unroll_info( $param_inst->type, $use_stmts );
- foreach ( $unroll_info['args'] as $uarg ) {
- $args[] = $uarg;
- }
- continue;
- }
-
- $arg_type_expr = $this->param_type_to_graphql_expr( $param_inst, $use_stmts );
- $arg_entry = array(
- 'name' => $param_inst->name,
- 'type_expr' => $arg_type_expr,
- 'description' => $param_inst->description,
- // `#[Parameter]`-declared args carry their shape inline and
- // have no separate reflector to read `#[Metadata]` from, so
- // the MVP exposes them without metadata.
- 'metadata' => array(),
- );
- if ( $param_inst->has_default ) {
- $arg_entry['default'] = $param_inst->default;
- }
- $args[] = $arg_entry;
- }
-
- // Merge ParameterDescription.
- $pd_attrs = $prop->getAttributes( ParameterDescription::class );
- foreach ( $pd_attrs as $pda ) {
- $pd_inst = $pda->newInstance();
- foreach ( $args as &$arg ) {
- if ( $arg['name'] === $pd_inst->name ) {
- if ( ! empty( $arg['description'] ) ) {
- $this->errors[] = "Property \"{$prop->getDeclaringClass()->getShortName()}::\${$prop->getName()}\": parameter \"{$pd_inst->name}\" has a description in both #[Parameter] and #[ParameterDescription].";
- }
- $arg['description'] = $pd_inst->description;
- }
- }
- }
- }
-
- // Flag connection fields that have pagination args so the template
- // can generate a resolve callback that slices the connection.
- $is_connection = $type_name === Connection::class;
- $has_pagination = $is_connection && ! empty( $args );
- $paginated_connection = $context === 'output' && $has_pagination;
-
- $field_context_label = $prop->getDeclaringClass()->getShortName() . '::$' . $prop->getName();
-
- $metadata_visible = $this->is_target_metadata_visible( $prop );
- $authorization = $this->resolve_field_authorization( $prop, $type_level_usages, $type_level_descriptors );
- $metadata_full = $this->harvest_metadata( $prop, $field_context_label );
- $metadata = $metadata_full;
-
- // Per-target `_apiMetadata` opt-out: when any attribute on the
- // property declares `shows_in_metadata_query(): false`, the
- // field's row is omitted entirely. The runtime gate is
- // unaffected — `attribute_expr` stays populated so the resolver
- // template still emits the `'resolve'` callback; we only blank
- // the descriptors and metadata that feed the discovery channel.
- // `metadata_runtime` keeps the full set: the gate threads it into
- // `$_metadata['field']`, and the opt-out is discovery-only.
- if ( ! $metadata_visible ) {
- $metadata = array();
- $authorization['descriptors'] = array();
- }
-
- return array(
- 'name' => $prop->getName(),
- 'type_expr' => $type_expr,
- 'description' => $description,
- 'args' => $args,
- 'deprecation_reason' => ! empty( $deprecation ) ? $deprecation[0]->newInstance()->reason : null,
- 'paginated_connection' => $paginated_connection,
- 'metadata' => $metadata,
- 'metadata_runtime' => $metadata_full,
- 'authorization' => $authorization,
- );
- }
-
- private function php_type_to_graphql_expr( string $type_name, bool $nullable, \ReflectionProperty|\ReflectionParameter $context, array &$use_stmts ): string {
- // Check for ScalarType attribute.
- $scalar_attr = $context->getAttributes( ScalarType::class );
- if ( ! empty( $scalar_attr ) ) {
- $scalar_class = $scalar_attr[0]->newInstance()->type;
- $scalar_short = ( new \ReflectionClass( $scalar_class ) )->getShortName();
- $alias = $scalar_short . 'Type';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Scalars\\{$scalar_short} as {$alias}";
- $expr = "{$alias}::get()";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Check for ArrayOf attribute.
- $array_of_attr = $context->getAttributes( ArrayOf::class );
- if ( ! empty( $array_of_attr ) && $type_name === 'array' ) {
- $item_type = $array_of_attr[0]->newInstance()->type;
- $item_expr = $this->type_string_to_graphql_expr( $item_type, $use_stmts );
- $expr = "Type::listOf(Type::nonNull({$item_expr}))";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Check for ConnectionOf attribute.
- $conn_attr = $context->getAttributes( ConnectionOf::class );
- if ( ! empty( $conn_attr ) ) {
- $node_type = $conn_attr[0]->newInstance()->type;
- $node_short = ( new \ReflectionClass( $node_type ) )->getShortName();
- $conn_alias = $node_short . 'ConnectionType';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Pagination\\{$node_short}Connection as {$conn_alias}";
-
- // Register the connection for generation.
- $this->connections[ $node_type ] = array(
- 'node_type' => $node_type,
- 'source' => $context instanceof \ReflectionProperty ? $context->getDeclaringClass()->getShortName() . '::$' . $context->getName() : 'return type',
- );
-
- $expr = "{$conn_alias}::get()";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Primitive types.
- $primitive = match ( $type_name ) {
- 'int' => 'Type::int()',
- 'float' => 'Type::float()',
- 'string' => 'Type::string()',
- 'bool' => 'Type::boolean()',
- default => null,
- };
-
- if ( $primitive !== null ) {
- return $nullable ? $primitive : "Type::nonNull({$primitive})";
- }
-
- // Enum or type reference.
- $class_info = $this->get_class_info( $type_name );
- if ( $class_info !== null ) {
- $short = ( new \ReflectionClass( $type_name ) )->getShortName();
- $expr = $this->class_info_to_graphql_expr( $class_info, $short, $use_stmts ) ?? 'Type::string()';
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Unknown type — fallback to string.
- $this->warnings[] = "Unknown type '{$type_name}', falling back to String.";
- return $nullable ? 'Type::string()' : 'Type::nonNull(Type::string())';
- }
-
- private function type_string_to_graphql_expr( string $type, array &$use_stmts ): string {
- // Primitive string references.
- return match ( $type ) {
- 'int' => 'Type::int()',
- 'float' => 'Type::float()',
- 'string' => 'Type::string()',
- 'bool' => 'Type::boolean()',
- default => $this->class_type_to_graphql_expr( $type, $use_stmts ),
- };
- }
-
- private function class_type_to_graphql_expr( string $fqcn, array &$use_stmts ): string {
- $class_info = $this->get_class_info( $fqcn );
- if ( $class_info === null ) {
- $this->warnings[] = "Unknown class type '{$fqcn}' in ArrayOf.";
- return 'Type::string()';
- }
-
- $short = ( new \ReflectionClass( $fqcn ) )->getShortName();
-
- return $this->class_info_to_graphql_expr( $class_info, $short, $use_stmts ) ?? 'Type::string()';
- }
-
- /**
- * Resolve a registered class kind into the GraphQL expression that references its generated type,
- * pushing the matching use-statement into $use_stmts as a side effect. Returns null for kinds this
- * builder does not know how to emit; callers decide what fallback to use.
- */
- private function class_info_to_graphql_expr( array $class_info, string $short, array &$use_stmts ): ?string {
- switch ( $class_info['kind'] ) {
- case 'enum':
- $alias = $short . 'Type';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Enums\\{$short} as {$alias}";
- return "{$alias}::get()";
-
- case 'type':
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Output\\{$short}";
- return "{$short}::get()";
-
- case 'input_type':
- $gen_name = str_ends_with( $short, 'Input' ) ? substr( $short, 0, -5 ) : $short;
- $alias = $gen_name . 'Input';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Input\\{$gen_name} as {$alias}";
- return "{$alias}::get()";
- }
-
- return null;
- }
-
- private function param_type_to_graphql_expr( Parameter $param, array &$use_stmts ): string {
- $base = match ( $param->type ) {
- 'int' => 'Type::int()',
- 'float' => 'Type::float()',
- 'string' => 'Type::string()',
- 'bool' => 'Type::boolean()',
- default => $this->class_type_to_graphql_expr( $param->type, $use_stmts ),
- };
-
- if ( $param->array ) {
- $base = "Type::listOf(Type::nonNull({$base}))";
- }
-
- if ( ! $param->nullable && ! $param->has_default ) {
- $base = "Type::nonNull({$base})";
- }
-
- return $base;
- }
-
- private function get_return_type_expr( \ReflectionMethod $method, array &$use_stmts ): string {
- $return_type = $method->getReturnType();
- if ( $return_type === null ) {
- return 'Type::string()';
- }
-
- $type_name = $return_type instanceof \ReflectionNamedType ? $return_type->getName() : 'mixed';
- $nullable = $return_type->allowsNull();
-
- // Check for ReturnType attribute (interface return).
- $return_type_attr = $method->getAttributes( ReturnType::class );
- if ( ! empty( $return_type_attr ) ) {
- $iface_class = $return_type_attr[0]->newInstance()->type;
- $iface_info = $this->get_class_info( $iface_class );
- if ( null !== $iface_info && 'interface' === $iface_info['kind'] ) {
- $iface_short = ( new \ReflectionClass( $iface_class ) )->getShortName();
- $alias = $iface_short . 'Interface';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Interfaces\\{$iface_short} as {$alias}";
- $expr = "{$alias}::get()";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
- }
-
- // Check for ConnectionOf on the method.
- $conn_attr = $method->getAttributes( ConnectionOf::class );
- if ( ! empty( $conn_attr ) ) {
- $node_type = $conn_attr[0]->newInstance()->type;
- $node_short = ( new \ReflectionClass( $node_type ) )->getShortName();
- $conn_alias = $node_short . 'ConnectionType';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Pagination\\{$node_short}Connection as {$conn_alias}";
-
- $this->connections[ $node_type ] = array(
- 'node_type' => $node_type,
- 'source' => $method->getDeclaringClass()->getShortName() . '::execute()',
- );
-
- $expr = "{$conn_alias}::get()";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Check for ArrayOf on the method (plain list return). Mirrors the
- // property-/parameter-level ArrayOf handling: a method declared
- // `: array` with `#[ArrayOf( X::class )]` becomes `[X!]!` (or `[X!]`
- // when the return type is nullable). The element expression is
- // resolved through the same helper the field path uses, so scalar
- // and object element types are handled identically.
- $array_of_attr = $method->getAttributes( ArrayOf::class );
- if ( ! empty( $array_of_attr ) && 'array' === $type_name ) {
- $item_type = $array_of_attr[0]->newInstance()->type;
- $item_expr = $this->type_string_to_graphql_expr( $item_type, $use_stmts );
- $expr = "Type::listOf(Type::nonNull({$item_expr}))";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Output type reference.
- $class_info = $this->get_class_info( $type_name );
- if ( $class_info !== null && $class_info['kind'] === 'type' ) {
- $short = ( new \ReflectionClass( $type_name ) )->getShortName();
- $alias = $short . 'Type';
- $use_stmts[] = $this->autogenerated_namespace . "\\GraphQLTypes\\Output\\{$short} as {$alias}";
- $expr = "{$alias}::get()";
- return $nullable ? $expr : "Type::nonNull({$expr})";
- }
-
- // Primitive return.
- $primitive = match ( $type_name ) {
- 'int' => 'Type::int()',
- 'float' => 'Type::float()',
- 'string' => 'Type::string()',
- 'bool' => 'Type::boolean()',
- default => 'Type::string()',
- };
-
- return $nullable ? $primitive : "Type::nonNull({$primitive})";
- }
-
- private function to_screaming_snake_case( string $pascal_case ): string {
- $result = preg_replace( '/([a-z])([A-Z])/', '$1_$2', $pascal_case );
- $result = preg_replace( '/([A-Z]+)([A-Z][a-z])/', '$1_$2', $result );
- return strtoupper( $result );
- }
-
- private function pascal_to_snake_case( string $pascal_case ): string {
- $result = preg_replace( '/([a-z])([A-Z])/', '$1_$2', $pascal_case );
- $result = preg_replace( '/([A-Z]+)([A-Z][a-z])/', '$1_$2', $result );
- return strtolower( $result );
- }
-
- /**
- * Compute the GraphQL field name to use for a query or mutation on the
- * root Query/Mutation type.
- *
- * GraphQL convention is PascalCase for type names and camelCase for
- * field names, so a class like `CreateProduct` becomes the field
- * `createProduct`. When the user explicitly supplied a `#[Name(...)]`
- * attribute we respect their string verbatim.
- */
- private function root_field_name( string $fqcn, \ReflectionClass $ref ): string {
- $graphql_name = $this->graphql_names[ $fqcn ];
- if ( ! empty( $ref->getAttributes( Name::class ) ) ) {
- return $graphql_name;
- }
- return lcfirst( $graphql_name );
- }
-
- private function format_with_phpcbf( string $file_path ): void {
- // Pass --standard explicitly: without it phpcbf reads the project's
- // phpcs.xml, whose Suin.Classes.PSR4 sniff aborts processing under
- // PHP 8.x with a "${var} in strings is deprecated" notice, leaving
- // the generated files unformatted.
- exec( escapeshellarg( $this->phpcbf_path ) . ' -q --standard=WordPress-Core ' . escapeshellarg( $file_path ) . ' 2>&1' );
- }
-
- private function rmdir_recursive( string $dir ): void {
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ),
- \RecursiveIteratorIterator::CHILD_FIRST
- );
-
- foreach ( $iterator as $file ) {
- if ( $file->isDir() ) {
- rmdir( $file->getPathname() );
- } else {
- unlink( $file->getPathname() );
- }
- }
-
- rmdir( $dir );
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/StalenessChecker.php b/plugins/woocommerce/bin/api-builder/StalenessChecker.php
deleted file mode 100644
index f81a30a3048..00000000000
--- a/plugins/woocommerce/bin/api-builder/StalenessChecker.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\DesignTime;
-
-/**
- * Checks whether the autogenerated API code is stale and needs rebuilding.
- *
- * Staleness is determined by hashing the contents of the code-API sources and
- * comparing the result to a hash recorded by {@see ApiBuilder} the last time
- * it ran. Using content hashes (rather than file mtimes) keeps the check
- * reliable in environments where mtimes don't reflect actual edit history,
- * such as fresh `git clone` checkouts and CI runners.
- */
-class StalenessChecker {
- private const HASH_FILE_NAME = 'api_source_hash.txt';
-
- /**
- * Returns true if the autogenerated code is stale (needs rebuilding).
- *
- * @param string|null $api_dir Absolute path to the code-API sources. Defaults to WooCommerce core's `src/Api`.
- * @param string|null $autogenerated_dir Absolute path to the autogenerated output directory. Defaults to WooCommerce core's `src/Internal/Api/Autogenerated`.
- */
- public static function is_stale( ?string $api_dir = null, ?string $autogenerated_dir = null ): bool {
- $api_dir ??= __DIR__ . '/../../src/Api';
- $autogenerated_dir ??= __DIR__ . '/../../src/Internal/Api/Autogenerated';
-
- $hash_file = $autogenerated_dir . '/' . self::HASH_FILE_NAME;
- if ( ! file_exists( $hash_file ) ) {
- return true;
- }
-
- $stored_hash = trim( (string) file_get_contents( $hash_file ) );
- return $stored_hash !== self::compute_source_hash( $api_dir );
- }
-
- /**
- * Returns a deterministic SHA-256 hash of every `.php` file under $api_dir.
- *
- * Files are sorted by their path relative to $api_dir before hashing so
- * the result does not depend on filesystem iteration order.
- *
- * @param string $api_dir Absolute path to the code-API sources directory.
- */
- public static function compute_source_hash( string $api_dir ): string {
- $files = self::collect_php_files( $api_dir );
- $hasher = hash_init( 'sha256' );
- foreach ( $files as $relative_path => $absolute_path ) {
- hash_update( $hasher, $relative_path );
- hash_update( $hasher, "\0" );
- hash_update_file( $hasher, $absolute_path );
- hash_update( $hasher, "\0" );
- }
- return hash_final( $hasher );
- }
-
- /**
- * Returns an associative array of relative path => absolute path for every
- * `.php` file under $dir, sorted by relative path.
- *
- * @return array<string, string>
- */
- private static function collect_php_files( string $dir ): array {
- $files = array();
-
- if ( ! is_dir( $dir ) ) {
- return $files;
- }
-
- $prefix_length = strlen( $dir ) + 1;
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS )
- );
-
- foreach ( $iterator as $file ) {
- if ( $file->isFile() && 'php' === $file->getExtension() ) {
- $files[ substr( $file->getPathname(), $prefix_length ) ] = $file->getPathname();
- }
- }
-
- ksort( $files );
- return $files;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/build-api.php b/plugins/woocommerce/bin/api-builder/build-api.php
deleted file mode 100644
index de8cfc64c98..00000000000
--- a/plugins/woocommerce/bin/api-builder/build-api.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// Refuse to run outside the CLI: this script wipes and regenerates the
-// Autogenerated/ directory, so a misconfigured web server that accidentally
-// serves this file could destroy the checked-in output on every hit.
-if ( PHP_SAPI !== 'cli' ) {
- http_response_code( 403 );
- exit;
-}
-
-$options = getopt(
- 'h',
- array(
- 'help',
- 'no-linter',
- 'api-dir:',
- 'autogen-dir:',
- 'api-namespace:',
- 'autogen-namespace:',
- 'composer-working-dir:',
- 'phpcbf-path:',
- )
-);
-
-if ( isset( $options['h'] ) || isset( $options['help'] ) ) {
- echo <<<'TXT'
-Usage: php build-api.php [options]
-
-When invoked without options the script regenerates WooCommerce core's
-GraphQL API. Sibling WooCommerce plugins that ship their own code-API
-classes can reuse the same script by passing all four --*-dir / --*-namespace
-flags to point at their own source and output trees.
-
-Options:
- --api-dir=PATH Directory containing code-API classes to scan.
- Default: WooCommerce core's src/Api.
- --autogen-dir=PATH Directory where generated code is written.
- WARNING: this directory is wiped on every run.
- Default: WooCommerce core's src/Internal/Api/Autogenerated.
- --api-namespace=NAMESPACE PSR-4 namespace that maps to --api-dir.
- Default: Automattic\WooCommerce\Api.
- --autogen-namespace=NS PSR-4 namespace that maps to --autogen-dir.
- Default: Automattic\WooCommerce\Internal\Api\Autogenerated.
- --composer-working-dir=DIR Directory passed to "composer dump-autoload"
- after generation. When --api-dir is NOT given
- (core build) this defaults to WooCommerce's
- plugin directory; when --api-dir IS given
- (plugin build) omission skips autoload
- regeneration so the plugin wrapper can drive
- composer itself.
- --phpcbf-path=PATH Path to a phpcbf executable used to format
- generated files.
- Default: WooCommerce's vendored phpcbf.
- --no-linter Skip the phpcbf pass entirely.
- -h, --help Show this message.
-
-TXT;
- exit( 0 );
-}
-
-// Enforce the "all or nothing" rule for the four core path/namespace flags:
-// mixing them (e.g. passing --api-dir but leaving --autogen-namespace at
-// core's default) would silently emit files into WooCommerce's own namespace
-// from an external plugin's source tree, which is almost certainly a mistake.
-$path_flags = array( 'api-dir', 'autogen-dir', 'api-namespace', 'autogen-namespace' );
-$provided_paths = array_filter( $path_flags, static fn( $f ) => isset( $options[ $f ] ) );
-if ( count( $provided_paths ) > 0 && count( $provided_paths ) < count( $path_flags ) ) {
- $missing = array_diff( $path_flags, $provided_paths );
- fwrite( STDERR, "Error: when overriding any of --api-dir / --autogen-dir / --api-namespace / --autogen-namespace, all four must be provided.\n" );
- fwrite( STDERR, 'Missing: --' . implode( ', --', $missing ) . "\n" );
- exit( 2 );
-}
-
-if ( PHP_VERSION_ID < 80100 ) {
- fwrite(
- STDERR,
- sprintf(
- "Error: PHP 8.1 or later is required to run the API build script. Current version: %s.\n",
- PHP_VERSION
- )
- );
- exit( 2 );
-}
-
-require_once __DIR__ . '/../../vendor/autoload.php';
-
-use Automattic\WooCommerce\Api\Infrastructure\DesignTime\ApiBuilder;
-
-$skip_linter = isset( $options['no-linter'] );
-
-$builder = new ApiBuilder(
- $options['api-dir'] ?? null,
- $options['autogen-dir'] ?? null,
- $options['api-namespace'] ?? null,
- $options['autogen-namespace'] ?? null,
- $options['composer-working-dir'] ?? null,
- $options['phpcbf-path'] ?? null,
-);
-$builder->build( $skip_linter );
diff --git a/plugins/woocommerce/bin/api-builder/check-api-staleness.php b/plugins/woocommerce/bin/api-builder/check-api-staleness.php
deleted file mode 100644
index 4f28e4b0a80..00000000000
--- a/plugins/woocommerce/bin/api-builder/check-api-staleness.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-if ( PHP_VERSION_ID < 80100 ) {
- fwrite(
- STDERR,
- sprintf(
- "Error: PHP 8.1 or later is required. Current version: %s.\n",
- PHP_VERSION
- )
- );
- exit( 2 );
-}
-
-require_once __DIR__ . '/../../vendor/autoload.php';
-
-use Automattic\WooCommerce\Api\Infrastructure\DesignTime\StalenessChecker;
-
-if ( StalenessChecker::is_stale() ) {
- fwrite( STDERR, "ERROR: Generated GraphQL API code is out of date.\n" );
- fwrite( STDERR, "Run 'pnpm run build:api' to regenerate.\n" );
- exit( 1 );
-}
-
-echo "GraphQL API code is up to date.\n";
-exit( 0 );
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/EnumTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/EnumTypeTemplate.php
deleted file mode 100644
index 7bca2055ffb..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/EnumTypeTemplate.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-/**
- * Template for generating a GraphQL EnumType class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var string $enum_fqcn
- * @var string $enum_alias
- * @var array $values - each: ['graphql_name', 'case_name', 'description', 'deprecation_reason' => ?string, 'metadata' => array]
- * @var array $metadata - type-level metadata, name => scalar value.
- */
-
-$escaped_description = addslashes( $description );
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-use <?php echo $enum_fqcn; ?> as <?php echo $enum_alias; ?>;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class <?php echo $class_name; ?> {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => '<?php echo $graphql_name; ?>',
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'values' => array(
-<?php foreach ( $values as $val ) : ?>
- '<?php echo $val['graphql_name']; ?>' => array(
- 'value' => <?php echo $enum_alias; ?>::<?php echo $val['case_name']; ?>,
- <?php if ( ! empty( $val['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $val['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $val['deprecation_reason'] ) ) : ?>
- 'deprecationReason' => '<?php echo addslashes( $val['deprecation_reason'] ); ?>',
-<?php endif; ?>
- <?php if ( ! empty( $val['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $val['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/GraphQLControllerTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/GraphQLControllerTemplate.php
deleted file mode 100644
index 21bd71e763b..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/GraphQLControllerTemplate.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-/**
- * Template for generating the GraphQLController subclass.
- *
- * Emitted by ApiBuilder. The generated class extends the public base
- * controller in Automattic\WooCommerce\Api\Infrastructure and overrides
- * build_schema() to reference the root types that ApiBuilder has just
- * generated in the same autogenerated namespace.
- *
- * When the build detected a `<api_namespace>\Infrastructure\ClassResolver`,
- * `<api_namespace>\Infrastructure\PrincipalResolver`, or
- * `<api_namespace>\Infrastructure\HttpStatusResolver` convention class, this
- * template also emits an override that wires it into the base controller —
- * routing class instantiation, per-request principal resolution, and
- * HTTP-status decisions respectively.
- *
- * @var string $namespace
- * @var ?string $class_resolver_fqcn - FQCN of the detected ClassResolver, or null.
- * @var ?string $principal_resolver_fqcn - FQCN of the detected PrincipalResolver, or null.
- * @var bool $principal_resolver_takes_request - true when resolve_principal() declares its \WP_REST_Request parameter; false when zero-arg.
- * @var ?string $status_resolver_fqcn - FQCN of the detected HttpStatusResolver, or null.
- */
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-
-class GraphQLController extends \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase {
- protected function build_schema(): Schema {
- return new Schema(
- array(
- 'query' => RootQueryType::get(),
- 'mutation' => RootMutationType::get(),
- 'types' => TypeRegistry::get_interface_implementors(),
- )
- );
- }
-<?php if ( null !== $class_resolver_fqcn ) : ?>
-
- protected function get_class_resolver_fqcn(): ?string {
- return \<?php echo $class_resolver_fqcn; ?>::class;
- }
-<?php endif; ?>
-<?php if ( null !== $principal_resolver_fqcn ) : ?>
-
- protected function get_principal_resolver_fqcn(): ?string {
- return \<?php echo $principal_resolver_fqcn; ?>::class;
- }
-
- protected function principal_resolver_takes_request(): bool {
- return <?php echo $principal_resolver_takes_request ? 'true' : 'false'; ?>;
- }
-<?php endif; ?>
-<?php if ( null !== $status_resolver_fqcn ) : ?>
-
- protected function get_status_resolver(): ?object {
- return new \<?php echo $status_resolver_fqcn; ?>();
- }
-<?php endif; ?>
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/InputObjectTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/InputObjectTypeTemplate.php
deleted file mode 100644
index d0d0c3a1336..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/InputObjectTypeTemplate.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-/**
- * Template for generating a GraphQL InputObjectType class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var array $use_statements
- * @var array $fields - each: ['name', 'type_expr', 'description', 'metadata' => array]
- * @var array $metadata - type-level metadata, name => scalar value.
- */
-
-$escaped_description = addslashes( $description );
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php
-// Drop any caller-supplied import whose effective short name would collide
-// with one of the hardcoded imports emitted below, otherwise the generated
-// file wouldn't compile ("Cannot use ... because the name is already in use").
-$reserved_short_names = array( 'InputObjectType', 'Type' );
-// PHP class-name resolution (including `use`) is case-insensitive, so the
-// collision check has to be too — a caller-supplied `Foo\type` would
-// otherwise slip past and fail at compile time of the generated file.
-$reserved_short_names_lower = array_map( 'strtolower', $reserved_short_names );
-$use_statements = array_values(
- array_filter(
- $use_statements,
- static function ( $use ) use ( $reserved_short_names_lower ) {
- $as_pos = stripos( $use, ' as ' );
- if ( false !== $as_pos ) {
- $short = trim( substr( $use, $as_pos + 4 ) );
- } else {
- $sep_pos = strrpos( $use, '\\' );
- $short = false !== $sep_pos ? substr( $use, $sep_pos + 1 ) : $use;
- }
- return ! in_array( strtolower( $short ), $reserved_short_names_lower, true );
- }
- )
-);
-?>
-<?php foreach ( $use_statements as $use ) : ?>
-use <?php echo $use; ?>;
-<?php endforeach; ?>
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class <?php echo $class_name; ?> {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => '<?php echo $graphql_name; ?>',
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
-<?php if ( ! empty( $authorization ) ) : ?>
- 'authorization' => array(
-<?php foreach ( $authorization as $descriptor ) : ?>
- array(
- 'attribute' => <?php echo var_export( $descriptor['attribute'], true ); ?>,
- 'args' => <?php echo var_export( $descriptor['args'], true ); ?>,
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'fields' => fn() => array(
-<?php foreach ( $fields as $field ) : ?>
- '<?php echo $field['name']; ?>' => array(
- 'type' => <?php echo $field['type_expr']; ?>,
- <?php if ( ! empty( $field['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $field['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $field['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $field['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/InterfaceTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/InterfaceTypeTemplate.php
deleted file mode 100644
index 1c447962df9..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/InterfaceTypeTemplate.php
+++ /dev/null
@@ -1,140 +0,0 @@
-<?php
-/**
- * Template for generating a GraphQL InterfaceType class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var array $use_statements
- * @var array $fields - each: ['name', 'type_expr', 'description', 'args' => [], 'deprecation_reason' => ?string, 'metadata' => array]
- * @var array $type_map - each: ['fqcn' => string, 'alias' => string] mapping PHP FQCN to generated ObjectType alias
- * @var array $metadata - type-level metadata, name => scalar value.
- */
-
-$escaped_description = addslashes( $description );
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php
-// Drop any caller-supplied import whose effective short name would collide
-// with one of the hardcoded imports emitted below, otherwise the generated
-// file wouldn't compile ("Cannot use ... because the name is already in use").
-$reserved_short_names = array( 'InterfaceType', 'Type' );
-// PHP class-name resolution (including `use`) is case-insensitive, so the
-// collision check has to be too — a caller-supplied `Foo\type` would
-// otherwise slip past and fail at compile time of the generated file.
-$reserved_short_names_lower = array_map( 'strtolower', $reserved_short_names );
-$use_statements = array_values(
- array_filter(
- $use_statements,
- static function ( $use ) use ( $reserved_short_names_lower ) {
- $as_pos = stripos( $use, ' as ' );
- if ( false !== $as_pos ) {
- $short = trim( substr( $use, $as_pos + 4 ) );
- } else {
- $sep_pos = strrpos( $use, '\\' );
- $short = false !== $sep_pos ? substr( $use, $sep_pos + 1 ) : $use;
- }
- return ! in_array( strtolower( $short ), $reserved_short_names_lower, true );
- }
- )
-);
-?>
-<?php foreach ( $use_statements as $use ) : ?>
-use <?php echo $use; ?>;
-<?php endforeach; ?>
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class <?php echo $class_name; ?> {
- private static ?InterfaceType $instance = null;
-
- public static function get(): InterfaceType {
- if ( null === self::$instance ) {
- self::$instance = new InterfaceType(
- array(
- 'name' => '<?php echo $graphql_name; ?>',
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
-<?php if ( ! empty( $authorization ) ) : ?>
- 'authorization' => array(
-<?php foreach ( $authorization as $descriptor ) : ?>
- array(
- 'attribute' => <?php echo var_export( $descriptor['attribute'], true ); ?>,
- 'args' => <?php echo var_export( $descriptor['args'], true ); ?>,
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'fields' => fn() => array(
-<?php foreach ( $fields as $field ) : ?>
- '<?php echo $field['name']; ?>' => array(
- 'type' => <?php echo $field['type_expr']; ?>,
- <?php if ( ! empty( $field['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $field['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $field['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $field['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- <?php if ( ! empty( $field['args'] ) ) : ?>
- 'args' => array(
- <?php foreach ( $field['args'] as $arg ) : ?>
- '<?php echo $arg['name']; ?>' => array(
- 'type' => <?php echo $arg['type_expr']; ?>,
- <?php if ( array_key_exists( 'default', $arg ) ) : ?>
- 'defaultValue' => <?php echo var_export( $arg['default'], true ); ?>,
-<?php endif; ?>
- <?php if ( ! empty( $arg['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $arg['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $arg['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $arg['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- <?php if ( ! empty( $field['deprecation_reason'] ) ) : ?>
- 'deprecationReason' => '<?php echo addslashes( $field['deprecation_reason'] ); ?>',
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
- 'resolveType' => function ( $value ) {
- $class = get_class( $value );
- $map = array(
-<?php foreach ( $type_map as $entry ) : ?>
- '<?php echo $entry['fqcn']; ?>' => <?php echo $entry['alias']; ?>::get(),
-<?php endforeach; ?>
- );
- return $map[ $class ] ?? null;
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/MutationResolverTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/MutationResolverTemplate.php
deleted file mode 100644
index 2a6393cebda..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/MutationResolverTemplate.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * Template for generating a mutation resolver class.
- * Identical structure to QueryResolverTemplate — mutations and queries follow the same resolver pattern.
- *
- * Variables: same as QueryResolverTemplate.php
- */
-
-// Re-use the query resolver template since the structure is identical.
-require __DIR__ . '/QueryResolverTemplate.php';
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/ObjectTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/ObjectTypeTemplate.php
deleted file mode 100644
index 75fd86a85bb..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/ObjectTypeTemplate.php
+++ /dev/null
@@ -1,203 +0,0 @@
-<?php
-/**
- * Template for generating a GraphQL ObjectType class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var array $use_statements
- * @var array $interfaces - each: ['alias' => string]
- * @var array $fields - each: ['name', 'type_expr', 'description', 'args' => [], 'deprecation_reason' => ?string, 'paginated_connection' => bool, 'metadata' => array, 'metadata_runtime' => array]
- * @var array $metadata - type-level metadata for discovery (`_apiMetadata`); blank when the type opts out via shows_in_metadata_query().
- * @var array $metadata_runtime - full type-level metadata, threaded into field gates' $_metadata['type'] slice regardless of discovery opt-out.
- */
-
-$escaped_description = addslashes( $description );
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php
-$has_paginated_connection = false;
-$has_authorized_field = false;
-foreach ( $fields as $f ) {
- if ( ! empty( $f['paginated_connection'] ) ) {
- $has_paginated_connection = true;
- }
- if ( ! empty( $f['authorization']['attribute_expr'] ) && 'true' !== $f['authorization']['attribute_expr'] ) {
- $has_authorized_field = true;
- }
-}
-$needs_utils_import = $has_paginated_connection || $has_authorized_field;
-// Drop any caller-supplied import whose effective short name would collide
-// with one of the hardcoded imports emitted below, otherwise the generated
-// file wouldn't compile ("Cannot use ... because the name is already in use").
-$reserved_short_names = array( 'ObjectType', 'Type' );
-if ( $has_paginated_connection ) {
- $reserved_short_names[] = 'Connection';
-}
-if ( $needs_utils_import ) {
- $reserved_short_names[] = 'ResolverHelpers';
-}
-// PHP class-name resolution (including `use`) is case-insensitive, so the
-// collision check has to be too — a caller-supplied `Foo\resolveinfo` would
-// otherwise slip past and fail at compile time of the generated file.
-$reserved_short_names_lower = array_map( 'strtolower', $reserved_short_names );
-$use_statements = array_values(
- array_filter(
- $use_statements,
- static function ( $use ) use ( $reserved_short_names_lower ) {
- $as_pos = stripos( $use, ' as ' );
- if ( false !== $as_pos ) {
- $short = trim( substr( $use, $as_pos + 4 ) );
- } else {
- $sep_pos = strrpos( $use, '\\' );
- $short = false !== $sep_pos ? substr( $use, $sep_pos + 1 ) : $use;
- }
- return ! in_array( strtolower( $short ), $reserved_short_names_lower, true );
- }
- )
-);
-?>
-<?php foreach ( $use_statements as $use ) : ?>
-use <?php echo $use; ?>;
-<?php endforeach; ?>
-<?php if ( $needs_utils_import ) : ?>
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-<?php endif; ?>
-<?php if ( $has_paginated_connection ) : ?>
-use Automattic\WooCommerce\Api\Pagination\Connection;
-<?php endif; ?>
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class <?php echo $class_name; ?> {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => '<?php echo $graphql_name; ?>',
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
-<?php if ( ! empty( $authorization ) ) : ?>
- 'authorization' => array(
-<?php foreach ( $authorization as $descriptor ) : ?>
- array(
- 'attribute' => <?php echo var_export( $descriptor['attribute'], true ); ?>,
- 'args' => <?php echo var_export( $descriptor['args'], true ); ?>,
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
-<?php if ( ! empty( $interfaces ) ) : ?>
- 'interfaces' => fn() => array(
- <?php foreach ( $interfaces as $iface ) : ?>
- <?php echo $iface['alias']; ?>::get(),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'fields' => fn() => array(
-<?php foreach ( $fields as $field ) : ?>
- '<?php echo $field['name']; ?>' => array(
- 'type' => <?php echo $field['type_expr']; ?>,
- <?php if ( ! empty( $field['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $field['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $field['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $field['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- <?php if ( ! empty( $field['authorization']['descriptors'] ) ) : ?>
- 'authorization' => array(
- <?php foreach ( $field['authorization']['descriptors'] as $descriptor ) : ?>
- array(
- 'attribute' => <?php echo var_export( $descriptor['attribute'], true ); ?>,
- 'args' => <?php echo var_export( $descriptor['args'], true ); ?>,
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- <?php if ( ! empty( $field['args'] ) ) : ?>
- 'args' => array(
- <?php foreach ( $field['args'] as $arg ) : ?>
- '<?php echo $arg['name']; ?>' => array(
- 'type' => <?php echo $arg['type_expr']; ?>,
- <?php if ( array_key_exists( 'default', $arg ) ) : ?>
- 'defaultValue' => <?php echo var_export( $arg['default'], true ); ?>,
-<?php endif; ?>
- <?php if ( ! empty( $arg['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $arg['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( ! empty( $arg['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $arg['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- <?php if ( ! empty( $field['deprecation_reason'] ) ) : ?>
- 'deprecationReason' => '<?php echo addslashes( $field['deprecation_reason'] ); ?>',
-<?php endif; ?>
- <?php
- $has_field_auth = ! empty( $field['authorization']['attribute_expr'] ) && 'true' !== $field['authorization']['attribute_expr'];
- $is_paginated = ! empty( $field['paginated_connection'] );
- $field_metadata_expr = var_export( $field['metadata_runtime'], true );
- $type_metadata_expr = var_export( $metadata_runtime, true );
- ?>
- <?php if ( $is_paginated ) : ?>
- 'complexity' => ResolverHelpers::complexity_from_pagination(...),
-<?php endif; ?>
- <?php if ( $has_field_auth ) : ?>
- 'resolve' => function( $parent, $args, $context ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => <?php echo $type_metadata_expr; ?>,
- 'field' => <?php echo $field_metadata_expr; ?>,
- );
- $_args = $args;
- $_parent = $parent;
- if ( ! ( <?php echo $field['authorization']['attribute_expr']; ?> ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, '<?php echo $graphql_name; ?>', '<?php echo $field['name']; ?>', '<?php echo $field['authorization']['first_attribute_short']; ?>' );
- }
- <?php if ( $is_paginated ) : ?>
- return ResolverHelpers::translate_exceptions( fn() => $parent-><?php echo $field['name']; ?>->slice( $args ) );
-<?php else : ?>
- return $parent-><?php echo $field['name']; ?>;
-<?php endif; ?>
- },
- <?php elseif ( $is_paginated ) : ?>
- 'resolve' => fn( $parent, array $args ): Connection => ResolverHelpers::translate_exceptions( fn() => $parent-><?php echo $field['name']; ?>->slice( $args ) ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/PageInfoTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/PageInfoTypeTemplate.php
deleted file mode 100644
index a450d00a83c..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/PageInfoTypeTemplate.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * Template for generating the shared PageInfo GraphQL type class.
- *
- * @var string $namespace
- */
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PageInfo {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'PageInfo',
- 'fields' => array(
- 'has_next_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'has_previous_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'start_cursor' => array(
- 'type' => Type::string(),
- ),
- 'end_cursor' => array(
- 'type' => Type::string(),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/QueryResolverTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/QueryResolverTemplate.php
deleted file mode 100644
index b758ab65e44..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/QueryResolverTemplate.php
+++ /dev/null
@@ -1,277 +0,0 @@
-<?php
-/**
- * Template for generating a query/mutation resolver class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var string $command_fqcn
- * @var string $command_alias
- * @var string $return_type_expr
- * @var array $use_statements
- * @var array $args - each: ['name', 'type_expr', 'description', 'has_default', 'default', 'metadata' => array]
- * @var array $metadata - root-field-level metadata for discovery (`_apiMetadata`); blank when the operation opts out via shows_in_metadata_query().
- * @var array $metadata_runtime - full root-field-level metadata, published into $context['_query_metadata'] for downstream field gates regardless of discovery opt-out.
- * @var bool $has_connection_of
- * @var string $connection_type_alias
- * @var bool $standalone_attribute_check - true when authorize() is absent and the attribute_expr is the sole authorization gate
- * @var string $attribute_expr - PHP expression (referencing local `$principal`) that evaluates to true iff the autodiscovered authorization attributes grant access
- * @var string $compute_preauthorized_param_type - typed parameter declaration for the generated compute_preauthorized() helper (e.g. `object` or `\WP_User`)
- * @var array $execute_params - each: ['name', 'conversion' => ?string, 'is_infrastructure' => bool, 'unroll' => ?array]
- * @var ?array $execute_principal_arg - if non-null, ['type_name' => string]: execute() declares a $_principal infra param
- * @var bool $execute_query_info_arg - true when execute() declares a $_query_info infra param
- * @var array $input_converters - each: ['method_name', 'input_fqcn', 'input_class', 'properties' => [['name', 'conversion']]]
- * @var ?array $authorize_param_names - if non-null, the authorize() method param names (subset of execute params)
- * @var bool $has_preauthorized - true when authorize() declares a bool $_preauthorized infrastructure param
- * @var string $preauthorized_expr - PHP expression that evaluates to the $_preauthorized bool at runtime
- * @var ?array $authorize_principal_arg - if non-null, ['type_name' => string]: authorize() declares a $_principal infra param
- * @var bool $authorize_query_info_arg - true when authorize() declares a $_query_info infra param
- * @var bool $scalar_return - true when execute() returns a scalar (bool, int, float, string)
- * @var ?string $class_resolver_fqcn - FQCN of a user-provided class resolver with static resolve_class(string): object; null for direct `new` instantiation
- */
-
-$escaped_description = addslashes( $description );
-$has_authorize = $authorize_param_names !== null;
-$any_query_info_arg = $execute_query_info_arg || $authorize_query_info_arg;
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-use <?php echo $command_fqcn; ?> as <?php echo $command_alias; ?>;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-<?php
-// Drop any caller-supplied import whose effective short name would collide
-// with one of the imports emitted unconditionally above and below, otherwise
-// the generated file would fail to compile ("Cannot use ... because the name
-// is already in use").
-$reserved_short_names = array( $command_alias, 'QueryInfoExtractor', 'ResolverHelpers', 'ResolveInfo', 'Type' );
-// PHP class-name resolution (including `use`) is case-insensitive, so the
-// collision check has to be too — a caller-supplied `Foo\resolveinfo` would
-// otherwise slip past and fail at compile time of the generated file.
-$reserved_short_names_lower = array_map( 'strtolower', $reserved_short_names );
-$use_statements = array_values(
- array_filter(
- $use_statements,
- static function ( $use ) use ( $reserved_short_names_lower ) {
- $as_pos = stripos( $use, ' as ' );
- if ( false !== $as_pos ) {
- $short = trim( substr( $use, $as_pos + 4 ) );
- } else {
- $sep_pos = strrpos( $use, '\\' );
- $short = false !== $sep_pos ? substr( $use, $sep_pos + 1 ) : $use;
- }
- return ! in_array( strtolower( $short ), $reserved_short_names_lower, true );
- }
- )
-);
-?>
-<?php foreach ( $use_statements as $use ) : ?>
-use <?php echo $use; ?>;
-<?php endforeach; ?>
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class <?php echo $class_name; ?> {
- public static function get_field_definition(): array {
- return array(
-<?php if ( $scalar_return ) : ?>
- 'type' => Type::nonNull(new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(array(
- 'name' => '<?php echo $class_name; ?>Result',
- 'fields' => array(
- 'result' => array( 'type' => <?php echo $return_type_expr; ?> ),
- ),
- ))),
-<?php else : ?>
- 'type' => <?php echo $return_type_expr; ?>,
-<?php endif; ?>
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
-<?php if ( ! empty( $authorization_descriptors ) ) : ?>
- 'authorization' => array(
-<?php foreach ( $authorization_descriptors as $descriptor ) : ?>
- array(
- 'attribute' => <?php echo var_export( $descriptor['attribute'], true ); ?>,
- 'args' => <?php echo var_export( $descriptor['args'], true ); ?>,
- ),
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'args' => array(
-<?php foreach ( $args as $arg ) : ?>
- '<?php echo $arg['name']; ?>' => array(
- 'type' => <?php echo $arg['type_expr']; ?>,
- <?php if ( ! empty( $arg['description'] ) ) : ?>
- 'description' => __( '<?php echo addslashes( $arg['description'] ); ?>', 'woocommerce' ),
-<?php endif; ?>
- <?php if ( $arg['has_default'] ) : ?>
- 'defaultValue' => <?php echo var_export( $arg['default'], true ); ?>,
-<?php endif; ?>
- <?php if ( ! empty( $arg['metadata'] ) ) : ?>
- 'metadata' => array(
- <?php foreach ( $arg['metadata'] as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- ),
-<?php endforeach; ?>
- ),
-<?php if ( $has_connection_of ) : ?>
- 'complexity' => ResolverHelpers::complexity_from_pagination(...),
-<?php endif; ?>
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
-<?php if ( $standalone_attribute_check ) : ?>
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
-<?php endif; ?>
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = <?php echo var_export( $metadata_runtime, true ); ?>;
-
-
-<?php if ( null !== $class_resolver_fqcn ) : ?>
- $command = \<?php echo $class_resolver_fqcn; ?>::resolve_class( <?php echo $command_alias; ?>::class );
-<?php else : ?>
- $command = new <?php echo $command_alias; ?>();
-<?php endif; ?>
-
-<?php if ( $any_query_info_arg ) : ?>
- $query_info = QueryInfoExtractor::extract_from_info( $info, $args );
-<?php endif; ?>
- $execute_args = array();
-<?php
-$pagination_fqcn = 'Automattic\\WooCommerce\\Api\\Pagination\\PaginationParams';
-foreach ( $execute_params as $param ) :
- if ( ! empty( $param['unroll'] ) && $param['unroll']['fqcn'] === $pagination_fqcn ) :
-?>
- $execute_args['<?php echo $param['name']; ?>'] = ResolverHelpers::create_pagination_params( $args );
-<?php elseif ( ! empty( $param['unroll'] ) ) : ?>
- $execute_args['<?php echo $param['name']; ?>'] = ResolverHelpers::create_input(
- fn() => new \<?php echo $param['unroll']['fqcn']; ?>(
-<?php foreach ( $param['unroll']['properties'] as $uprop ) : ?>
- <?php echo $uprop['name']; ?>: <?php echo $uprop['value_expr']; ?>,
-<?php endforeach; ?>
- )
- );
-<?php elseif ( $param['is_infrastructure'] && $param['name'] === '_query_info' ) : ?>
- $execute_args['_query_info'] = $query_info;
-<?php elseif ( $param['is_infrastructure'] && $param['name'] === '_principal' ) : ?>
- $execute_args['_principal'] = $context['principal'];
-<?php elseif ( ! empty( $param['conversion'] ) ) : ?>
- if ( array_key_exists( '<?php echo $param['name']; ?>', $args ) ) {
- $execute_args['<?php echo $param['name']; ?>'] = <?php echo $param['conversion']; ?>;
- }
-<?php else : ?>
- if ( array_key_exists( '<?php echo $param['name']; ?>', $args ) ) {
- $execute_args['<?php echo $param['name']; ?>'] = $args['<?php echo $param['name']; ?>'];
- }
-<?php endif; ?>
-<?php endforeach; ?>
-
-<?php foreach ( $input_side_gates as $gate_set ) : ?>
- if ( isset( $execute_args['<?php echo $gate_set['exec_arg_name']; ?>'] ) && $execute_args['<?php echo $gate_set['exec_arg_name']; ?>'] instanceof \<?php echo $gate_set['input_fqcn']; ?> ) {
- $_parent = $execute_args['<?php echo $gate_set['exec_arg_name']; ?>'];
- <?php foreach ( $gate_set['fields'] as $field_gate ) : ?>
- if ( $_parent->was_provided( '<?php echo $field_gate['field_name']; ?>' ) ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => <?php echo $field_gate['type_metadata_literal']; ?>,
- 'field' => <?php echo $field_gate['field_metadata_literal']; ?>,
- );
- $_args = $args;
- if ( ! ( <?php echo $field_gate['attribute_expr']; ?> ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, '<?php echo $gate_set['input_short_name']; ?>', '<?php echo $field_gate['field_name']; ?>', '<?php echo $field_gate['first_attribute_short']; ?>' );
- }
- }
- <?php endforeach; ?>
- }
-<?php endforeach; ?>
-
-<?php if ( $has_authorize ) : ?>
- if ( ! ResolverHelpers::authorize_command( $command, array(
-<?php foreach ( $authorize_param_names as $name ) : ?>
- '<?php echo $name; ?>' => $execute_args['<?php echo $name; ?>'],
-<?php endforeach; ?>
-<?php if ( null !== $authorize_principal_arg ) : ?>
- '_principal' => $context['principal'],
-<?php endif; ?>
-<?php if ( $authorize_query_info_arg ) : ?>
- '_query_info' => $query_info,
-<?php endif; ?>
-<?php if ( $has_preauthorized ) : ?>
- '_preauthorized' => <?php echo $preauthorized_expr; ?>,
-<?php endif; ?>
- ) ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
-<?php endif; ?>
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
-<?php if ( $scalar_return ) : ?>
- return array( 'result' => $result );
-<?php else : ?>
- return $result;
-<?php endif; ?>
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( <?php echo $compute_preauthorized_param_type; ?> $principal ): bool {
- return <?php echo $attribute_expr; ?>;
- }
-<?php foreach ( $input_converters as $converter ) : ?>
-
- private static function <?php echo $converter['method_name']; ?>( array $data ): \<?php echo $converter['input_fqcn']; ?> {
- $input = new \<?php echo $converter['input_fqcn']; ?>();
-
- <?php foreach ( $converter['properties'] as $prop ) : ?>
- if ( array_key_exists( '<?php echo $prop['name']; ?>', $data ) ) {
- $input->mark_provided( '<?php echo $prop['name']; ?>' );
- <?php if ( ! empty( $prop['conversion'] ) ) : ?>
- $input-><?php echo $prop['name']; ?> = <?php echo $prop['conversion']; ?>;
-<?php else : ?>
- $input-><?php echo $prop['name']; ?> = $data['<?php echo $prop['name']; ?>'];
-<?php endif; ?>
- }
-<?php endforeach; ?>
-
- return $input;
- }
-<?php endforeach; ?>
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/RootMutationTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/RootMutationTypeTemplate.php
deleted file mode 100644
index e73a4216383..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/RootMutationTypeTemplate.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-/**
- * Template for generating the RootMutationType class.
- *
- * @var string $namespace
- * @var array $mutations - each: ['class_name', 'fqcn', 'graphql_name']
- */
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php foreach ( $mutations as $mutation ) : ?>
-use <?php echo $mutation['fqcn']; ?>;
-<?php endforeach; ?>
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootMutationType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Mutation',
- 'fields' => fn() => array(
-<?php foreach ( $mutations as $mutation ) : ?>
- '<?php echo $mutation['graphql_name']; ?>' => <?php echo $mutation['class_name']; ?>::get_field_definition(),
-<?php endforeach; ?>
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/RootQueryTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/RootQueryTypeTemplate.php
deleted file mode 100644
index 92635e958f4..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/RootQueryTypeTemplate.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-/**
- * Template for generating the RootQueryType class.
- *
- * Besides the autogenerated query fields, the root `Query` type also gets the
- * hand-written `_apiMetadata` field contributed by {@see \Automattic\WooCommerce\Api\Infrastructure\MetadataController}.
- * That class lives in the shared infrastructure namespace, so plugins reusing
- * this template inherit the metadata-discovery field for free.
- *
- * @var string $namespace
- * @var array $queries - each: ['class_name', 'fqcn', 'graphql_name']
- */
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php foreach ( $queries as $query ) : ?>
-use <?php echo $query['fqcn']; ?>;
-<?php endforeach; ?>
-use Automattic\WooCommerce\Api\Infrastructure\MetadataController;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootQueryType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => fn() => array(
-<?php foreach ( $queries as $query ) : ?>
- '<?php echo $query['graphql_name']; ?>' => <?php echo $query['class_name']; ?>::get_field_definition(),
-<?php endforeach; ?>
- MetadataController::FIELD_NAME => MetadataController::get_field_definition(),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/ScalarTypeTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/ScalarTypeTemplate.php
deleted file mode 100644
index 900a2fd27e7..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/ScalarTypeTemplate.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-/**
- * Template for generating a GraphQL CustomScalarType class.
- *
- * @var string $namespace
- * @var string $class_name
- * @var string $graphql_name
- * @var string $description
- * @var string $scalar_fqcn
- * @var string $scalar_alias
- * @var array $metadata - type-level metadata, name => scalar value.
- */
-
-$escaped_description = addslashes( $description );
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-use <?php echo $scalar_fqcn; ?> as <?php echo $scalar_alias; ?>;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-
-class <?php echo $class_name; ?> {
- private static ?CustomScalarType $instance = null;
-
- public static function get(): CustomScalarType {
- if ( null === self::$instance ) {
- self::$instance = new CustomScalarType(
- array(
- 'name' => '<?php echo $graphql_name; ?>',
-<?php if ( $description !== '' ) : ?>
- 'description' => __( '<?php echo $escaped_description; ?>', 'woocommerce' ),
-<?php endif; ?>
-<?php if ( ! empty( $metadata ) ) : ?>
- 'metadata' => array(
-<?php foreach ( $metadata as $meta_name => $meta_value ) : ?>
- <?php echo var_export( $meta_name, true ); ?> => <?php echo var_export( $meta_value, true ); ?>,
-<?php endforeach; ?>
- ),
-<?php endif; ?>
- 'serialize' => fn( $value ) => <?php echo $scalar_alias; ?>::serialize( $value ),
- 'parseValue' => function ( $value ) {
- try {
- return <?php echo $scalar_alias; ?>::parse( $value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- },
- 'parseLiteral' => function ( $value_node, ?array $variables = null ) {
- if ( $value_node instanceof \Automattic\WooCommerce\Api\Infrastructure\Schema\AST\StringValueNode ) {
- try {
- return <?php echo $scalar_alias; ?>::parse( $value_node->value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- }
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error(
- '<?php echo $graphql_name; ?> must be a string, got: ' . $value_node->kind
- );
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/bin/api-builder/code-templates/TypeRegistryTemplate.php b/plugins/woocommerce/bin/api-builder/code-templates/TypeRegistryTemplate.php
deleted file mode 100644
index 0538ffaeef2..00000000000
--- a/plugins/woocommerce/bin/api-builder/code-templates/TypeRegistryTemplate.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-/**
- * Template for generating the TypeRegistry class.
- *
- * Lists all concrete types that implement interfaces, so the schema
- * can register them for inline fragment resolution.
- *
- * @var string $namespace
- * @var array $types - each: ['short_name', 'fqcn']
- */
-?>
-<?php echo '<?php'; ?>
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace <?php echo $namespace; ?>;
-
-<?php foreach ( $types as $type ) : ?>
-use <?php echo $type['fqcn']; ?>;
-<?php endforeach; ?>
-
-class TypeRegistry {
- /**
- * Return all concrete types that implement interfaces.
- *
- * Pass this to the Schema 'types' config so that inline fragments
- * (e.g. `... on VariableProduct`) are resolvable.
- *
- * @return array
- */
- public static function get_interface_implementors(): array {
- return array(
-<?php foreach ( $types as $type ) : ?>
- <?php echo $type['short_name']; ?>::get(),
-<?php endforeach; ?>
- );
- }
-}
diff --git a/plugins/woocommerce/changelog/update-extract-dual-api-to-plugin b/plugins/woocommerce/changelog/update-extract-dual-api-to-plugin
new file mode 100644
index 00000000000..ece3006ace3
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-extract-dual-api-to-plugin
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Move the experimental dual API engine out of WooCommerce core into the WooCommerce Dual API plugin.
diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json
index f787de2907c..0d0b85b5f1c 100644
--- a/plugins/woocommerce/composer.json
+++ b/plugins/woocommerce/composer.json
@@ -92,8 +92,7 @@
"autoload": {
"exclude-from-classmap": [
"includes/legacy",
- "includes/libraries",
- "src/Internal/Api/DesignTime"
+ "includes/libraries"
],
"classmap": [
"includes/rest-api"
@@ -109,15 +108,13 @@
"src/StoreApi/deprecated.php",
"src/StoreApi/functions.php",
"src/Blocks/Domain/Services/functions.php",
- "src/Deprecated.php",
- "src/Api/Infrastructure/Schema/aliases.php"
+ "src/Deprecated.php"
]
},
"autoload-dev": {
"psr-4": {
"Automattic\\WooCommerce\\Tests\\": "tests/php/src/",
- "Automattic\\WooCommerce\\Testing\\Tools\\": "tests/Tools/",
- "Automattic\\WooCommerce\\Api\\Infrastructure\\DesignTime\\": "bin/api-builder/"
+ "Automattic\\WooCommerce\\Testing\\Tools\\": "tests/Tools/"
},
"classmap": [
"tests/legacy/unit-tests/rest-api/Helpers"
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 4f00fbe5dd0..79de3efa34f 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -449,9 +449,6 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\ProductFilters\MainQueryController::class )->register();
$container->get( Automattic\WooCommerce\Internal\ProductFilters\CacheController::class )->register();
- // Code+GraphQL API.
- Automattic\WooCommerce\Api\Infrastructure\Main::register();
-
// Integration point between legacy reports and orders APIs (the reports caches invalidation focused).
\WC_Admin_Reports::register_orders_hook_handlers();
}
diff --git a/plugins/woocommerce/lib/README.md b/plugins/woocommerce/lib/README.md
index e8ee558776c..e7229b44ce0 100644
--- a/plugins/woocommerce/lib/README.md
+++ b/plugins/woocommerce/lib/README.md
@@ -43,20 +43,6 @@ the root autoloader.
2. Add package slug to `extra/mozart/excluded-packages` section of `composer.json`
3. Run `composer run-script build-lib` from the root directory (You **should not** see the package in `packages/VendorName/PackageName` or `classes`) - see the note about MobileDetect below.
-## A note about the webonyx/graphql-php library
-
-Mozart rewrites namespace declarations and `use` statements, but it can miss stringified FQCNs
-(class names embedded in string literals). Before shipping a webonyx version bump, audit the
-rebuilt package for any such strings that still reference the bare `GraphQL\` namespace by
-running this from `plugins/woocommerce/`:
-
-```sh
-grep -rn "'GraphQL\\\\\\|\"GraphQL\\\\" lib/packages/GraphQL/
-```
-
-The grep should return no results. If it does, patch the offending file manually and commit it,
-mirroring the MobileDetect workflow below.
-
## A note about the MobileDetect library
The `lib/packages/Detection/MobileDetect.php` file
diff --git a/plugins/woocommerce/lib/composer.json b/plugins/woocommerce/lib/composer.json
index a91c9fc5d34..fe2cea4c86d 100644
--- a/plugins/woocommerce/lib/composer.json
+++ b/plugins/woocommerce/lib/composer.json
@@ -10,8 +10,7 @@
"mobiledetect/mobiledetectlib": "^3.74",
"psr/container": "^1.1",
"pelago/emogrifier": "7.3.0",
- "league/iso3166": "^4.3.3",
- "webonyx/graphql-php": "^15.31"
+ "league/iso3166": "^4.3.3"
},
"config": {
"platform": {
@@ -34,8 +33,7 @@
"psr/container",
"mobiledetect/mobiledetectlib",
"pelago/emogrifier",
- "league/iso3166",
- "webonyx/graphql-php"
+ "league/iso3166"
],
"excluded_packages": [
],
diff --git a/plugins/woocommerce/lib/composer.lock b/plugins/woocommerce/lib/composer.lock
index a696ad3ccc9..6e80eba77bb 100644
--- a/plugins/woocommerce/lib/composer.lock
+++ b/plugins/woocommerce/lib/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6bc80befedb70a96c4cafa8e061bc629",
+ "content-hash": "c6f3b99d2d3a16b509155f7eb35f0b19",
"packages": [],
"packages-dev": [
{
@@ -483,86 +483,6 @@
}
],
"time": "2025-01-02T08:10:11+00:00"
- },
- {
- "name": "webonyx/graphql-php",
- "version": "v15.32.3",
- "source": {
- "type": "git",
- "url": "https://github.com/webonyx/graphql-php.git",
- "reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/993bf0bea17f870412ad8a90f60c41cb8d5f1145",
- "reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "ext-mbstring": "*",
- "php": "^7.4 || ^8"
- },
- "require-dev": {
- "amphp/amp": "^2.6 || ^3",
- "amphp/http-server": "^2.1 || ^3",
- "dms/phpunit-arraysubset-asserts": "dev-master",
- "ergebnis/composer-normalize": "^2.28",
- "friendsofphp/php-cs-fixer": "3.95.1",
- "mll-lab/php-cs-fixer-config": "5.13.0",
- "nyholm/psr7": "^1.5",
- "phpbench/phpbench": "^1.2",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "2.1.51",
- "phpstan/phpstan-phpunit": "2.0.16",
- "phpstan/phpstan-strict-rules": "2.0.10",
- "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
- "psr/http-message": "^1 || ^2",
- "react/http": "^1.6",
- "react/promise": "^2.0 || ^3.0",
- "rector/rector": "^2.0",
- "symfony/polyfill-php81": "^1.23",
- "symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
- "thecodingmachine/safe": "^1.3 || ^2 || ^3",
- "ticketswap/phpstan-error-formatter": "1.3.0"
- },
- "suggest": {
- "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)",
- "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
- "psr/http-message": "To use standard GraphQL server",
- "react/promise": "To leverage async resolving on React PHP platform"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "GraphQL\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "A PHP port of GraphQL reference implementation",
- "homepage": "https://github.com/webonyx/graphql-php",
- "keywords": [
- "api",
- "graphql"
- ],
- "support": {
- "issues": "https://github.com/webonyx/graphql-php/issues",
- "source": "https://github.com/webonyx/graphql-php/tree/v15.32.3"
- },
- "funding": [
- {
- "url": "https://github.com/spawnia",
- "type": "github"
- },
- {
- "url": "https://opencollective.com/webonyx-graphql-php",
- "type": "open_collective"
- }
- ],
- "time": "2026-04-24T13:49:35+00:00"
}
],
"aliases": [],
@@ -577,5 +497,5 @@
"platform-overrides": {
"php": "7.4"
},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Deferred.php b/plugins/woocommerce/lib/packages/GraphQL/Deferred.php
deleted file mode 100644
index 00880a2e930..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Deferred.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseQueue;
-
-/**
- * User-facing promise class for deferred field resolution.
- *
- * @phpstan-type Executor callable(): mixed
- */
-class Deferred extends SyncPromise
-{
- /**
- * Executor for deferred promises.
- *
- * @var (callable(): mixed)|null
- */
- protected $executor;
-
- /**
- * Create a new Deferred promise and enqueue its execution.
- *
- * @api
- *
- * @param Executor $executor
- */
- public function __construct(callable $executor)
- {
- $this->executor = $executor;
-
- SyncPromiseQueue::enqueue(function (): void {
- $executor = $this->executor;
- assert($executor !== null, 'Always set in constructor, this callback runs only once.');
- $this->executor = null;
-
- try {
- $this->resolve($executor());
- } catch (\Throwable $e) {
- $this->reject($e);
- }
- });
- }
-
- /**
- * Alias for __construct.
- *
- * @param Executor $executor
- *
- * @deprecated TODO remove in next major version, use new Deferred() instead
- */
- public static function create(callable $executor): self
- {
- return new self($executor);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php b/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php
deleted file mode 100644
index 6132a33dc4b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Implementing ClientAware allows graphql-php to decide if this error is safe to be shown to clients.
- *
- * Only errors that both implement this interface and return true from `isClientSafe()`
- * will retain their original error message during formatting.
- *
- * All other errors will have their message replaced with "Internal server error".
- */
-interface ClientAware
-{
- /**
- * Is it safe to show the error message to clients?
- *
- * @api
- */
- public function isClientSafe(): bool;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php
deleted file mode 100644
index 084040dfc72..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-type InputPath list<string|int>
- */
-class CoercionError extends Error
-{
- /** @var InputPath|null */
- public ?array $inputPath;
-
- /** @var mixed whatever invalid value was passed */
- public $invalidValue;
-
- /**
- * @param InputPath|null $inputPath
- * @param mixed $invalidValue whatever invalid value was passed
- *
- * @return static
- */
- public static function make(
- string $message,
- ?array $inputPath,
- $invalidValue,
- ?\Throwable $previous = null
- ): self {
- $instance = new static($message, null, null, [], null, $previous);
- $instance->inputPath = $inputPath;
- $instance->invalidValue = $invalidValue;
-
- return $instance;
- }
-
- public function printInputPath(): ?string
- {
- if ($this->inputPath === null) {
- return null;
- }
-
- $path = '';
- foreach ($this->inputPath as $segment) {
- $path .= is_int($segment)
- ? "[{$segment}]"
- : ".{$segment}";
- }
-
- return $path;
- }
-
- public function printInvalidValue(): string
- {
- return Utils::printSafeJson($this->invalidValue);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php b/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php
deleted file mode 100644
index 943299bf380..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Collection of flags for [error debugging](error-handling.md#debugging-tools).
- */
-final class DebugFlag
-{
- public const NONE = 0;
- public const INCLUDE_DEBUG_MESSAGE = 1;
- public const INCLUDE_TRACE = 2;
- public const RETHROW_INTERNAL_EXCEPTIONS = 4;
- public const RETHROW_UNSAFE_EXCEPTIONS = 8;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php b/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php
deleted file mode 100644
index a72d276ada4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php
+++ /dev/null
@@ -1,319 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\SourceLocation;
-
-/**
- * Describes an Error found during the parse, validate, or
- * execute phases of performing a Automattic\WooCommerce\Vendor\GraphQL operation. In addition to a message
- * and stack trace, it also includes information about the locations in a
- * Automattic\WooCommerce\Vendor\GraphQL document and/or execution result that correspond to the Error.
- *
- * When the error was caused by an exception thrown in resolver, original exception
- * is available via `getPrevious()`.
- *
- * Also read related docs on [error handling](error-handling.md)
- *
- * Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
- * are available in addition to those listed below.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Error\ErrorTest
- */
-class Error extends \Exception implements \JsonSerializable, ClientAware, ProvidesExtensions
-{
- /**
- * Lazily initialized.
- *
- * @var array<int, SourceLocation>
- */
- private array $locations;
-
- /**
- * An array describing the JSON-path into the execution response which
- * corresponds to this error. Only included for errors during execution.
- * When fields are aliased, the path includes aliases.
- *
- * @var list<int|string>|null
- */
- public ?array $path;
-
- /**
- * An array describing the JSON-path into the execution response which
- * corresponds to this error. Only included for errors during execution.
- * This will never include aliases.
- *
- * @var list<int|string>|null
- */
- public ?array $unaliasedPath;
-
- /**
- * An array of Automattic\WooCommerce\Vendor\GraphQL AST Nodes corresponding to this error.
- *
- * @var array<Node>|null
- */
- public ?array $nodes;
-
- /**
- * The source Automattic\WooCommerce\Vendor\GraphQL document for the first location of this error.
- *
- * Note that if this Error represents more than one node, the source may not
- * represent nodes after the first node.
- */
- private ?Source $source;
-
- /** @var array<int, int>|null */
- private ?array $positions;
-
- private bool $isClientSafe;
-
- /** @var array<string, mixed>|null */
- protected ?array $extensions;
-
- /**
- * @param iterable<array-key, Node|null>|Node|null $nodes
- * @param array<int, int>|null $positions
- * @param list<int|string>|null $path
- * @param array<string, mixed>|null $extensions
- * @param list<int|string>|null $unaliasedPath
- */
- public function __construct(
- string $message = '',
- $nodes = null,
- ?Source $source = null,
- ?array $positions = null,
- ?array $path = null,
- ?\Throwable $previous = null,
- ?array $extensions = null,
- ?array $unaliasedPath = null
- ) {
- parent::__construct($message, 0, $previous);
-
- // Compute list of blame nodes.
- if ($nodes instanceof \Traversable) {
- /** @phpstan-ignore arrayFilter.strict */
- $this->nodes = array_filter(iterator_to_array($nodes));
- } elseif (is_array($nodes)) {
- $this->nodes = array_filter($nodes);
- } elseif ($nodes !== null) {
- $this->nodes = [$nodes];
- } else {
- $this->nodes = null;
- }
-
- $this->source = $source;
- $this->positions = $positions;
- $this->path = $path;
- $this->unaliasedPath = $unaliasedPath;
-
- if (is_array($extensions) && $extensions !== []) {
- $this->extensions = $extensions;
- } elseif ($previous instanceof ProvidesExtensions) {
- $this->extensions = $previous->getExtensions();
- } else {
- $this->extensions = null;
- }
-
- $this->isClientSafe = $previous instanceof ClientAware
- ? $previous->isClientSafe()
- : $previous === null;
- }
-
- /**
- * Given an arbitrary Error, presumably thrown while attempting to execute a
- * Automattic\WooCommerce\Vendor\GraphQL operation, produce a new GraphQLError aware of the location in the
- * document responsible for the original Error.
- *
- * @param mixed $error
- * @param iterable<Node>|Node|null $nodes
- * @param list<int|string>|null $path
- * @param list<int|string>|null $unaliasedPath
- */
- public static function createLocatedError($error, $nodes = null, ?array $path = null, ?array $unaliasedPath = null): Error
- {
- if ($error instanceof self) {
- if ($error->isLocated()) {
- return $error;
- }
-
- $nodes ??= $error->getNodes();
- $path ??= $error->getPath();
- $unaliasedPath ??= $error->getUnaliasedPath();
- }
-
- $source = null;
- $originalError = null;
- $positions = [];
- $extensions = [];
-
- if ($error instanceof self) {
- $message = $error->getMessage();
- $originalError = $error;
- $source = $error->getSource();
- $positions = $error->getPositions();
- $extensions = $error->getExtensions();
- } elseif ($error instanceof InvariantViolation) {
- $message = $error->getMessage();
- $originalError = $error->getPrevious() ?? $error;
- } elseif ($error instanceof \Throwable) {
- $message = $error->getMessage();
- $originalError = $error;
- } else {
- $message = (string) $error;
- }
-
- $nonEmptyMessage = $message === ''
- ? 'An unknown error occurred.'
- : $message;
-
- return new static(
- $nonEmptyMessage,
- $nodes,
- $source,
- $positions,
- $path,
- $originalError,
- $extensions,
- $unaliasedPath
- );
- }
-
- protected function isLocated(): bool
- {
- $path = $this->getPath();
- $nodes = $this->getNodes();
-
- return $path !== null
- && $path !== []
- && $nodes !== null
- && $nodes !== [];
- }
-
- public function isClientSafe(): bool
- {
- return $this->isClientSafe;
- }
-
- public function getSource(): ?Source
- {
- return $this->source
- ??= $this->nodes[0]->loc->source
- ?? null;
- }
-
- /** @return array<int, int> */
- public function getPositions(): array
- {
- if (! isset($this->positions)) {
- $this->positions = [];
-
- if (isset($this->nodes)) {
- foreach ($this->nodes as $node) {
- if (isset($node->loc->start)) {
- $this->positions[] = $node->loc->start;
- }
- }
- }
- }
-
- return $this->positions;
- }
-
- /**
- * An array of locations within the source Automattic\WooCommerce\Vendor\GraphQL document which correspond to this error.
- *
- * Each entry has information about `line` and `column` within source Automattic\WooCommerce\Vendor\GraphQL document:
- * $location->line;
- * $location->column;
- *
- * Errors during validation often contain multiple locations, for example to
- * point out to field mentioned in multiple fragments. Errors during execution include a
- * single location, the field which produced the error.
- *
- * @return array<int, SourceLocation>
- *
- * @api
- */
- public function getLocations(): array
- {
- if (! isset($this->locations)) {
- $positions = $this->getPositions();
- $source = $this->getSource();
- $nodes = $this->getNodes();
-
- $this->locations = [];
- if ($source !== null && $positions !== []) {
- foreach ($positions as $position) {
- $this->locations[] = $source->getLocation($position);
- }
- } elseif ($nodes !== null && $nodes !== []) {
- foreach ($nodes as $node) {
- if (isset($node->loc->source)) {
- $this->locations[] = $node->loc->source->getLocation($node->loc->start);
- }
- }
- }
- }
-
- return $this->locations;
- }
-
- /** @return array<Node>|null */
- public function getNodes(): ?array
- {
- return $this->nodes;
- }
-
- /**
- * Returns an array describing the path from the root value to the field which produced this error.
- * Only included for execution errors. When fields are aliased, the path includes aliases.
- *
- * @return list<int|string>|null
- *
- * @api
- */
- public function getPath(): ?array
- {
- return $this->path;
- }
-
- /**
- * Returns an array describing the path from the root value to the field which produced this error.
- * Only included for execution errors. This will never include aliases.
- *
- * @return list<int|string>|null
- *
- * @api
- */
- public function getUnaliasedPath(): ?array
- {
- return $this->unaliasedPath;
- }
-
- /** @return array<string, mixed>|null */
- public function getExtensions(): ?array
- {
- return $this->extensions;
- }
-
- /**
- * Specify data which should be serialized to JSON.
- *
- * @see http://php.net/manual/en/jsonserializable.jsonserialize.php
- *
- * @return array<string, mixed> data which can be serialized by <b>json_encode</b>,
- * which is a value of any type other than a resource
- */
- #[\ReturnTypeWillChange]
- public function jsonSerialize(): array
- {
- return FormattedError::createFromException($this);
- }
-
- public function __toString(): string
- {
- return FormattedError::printError($this);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php
deleted file mode 100644
index 4d0639f5dab..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php
+++ /dev/null
@@ -1,337 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\SourceLocation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use PHPUnit\Framework\Test;
-
-/**
- * This class is used for [default error formatting](error-handling.md).
- * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
- * and provides tools for error debugging.
- *
- * @see ExecutionResult
- *
- * @phpstan-import-type SerializableError from ExecutionResult
- * @phpstan-import-type ErrorFormatter from ExecutionResult
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Error\FormattedErrorTest
- */
-class FormattedError
-{
- private static string $internalErrorMessage = 'Internal server error';
-
- /**
- * Set default error message for internal errors formatted using createFormattedError().
- * This value can be overridden by passing 3rd argument to `createFormattedError()`.
- *
- * @api
- */
- public static function setInternalErrorMessage(string $msg): void
- {
- self::$internalErrorMessage = $msg;
- }
-
- /**
- * Prints a GraphQLError to a string, representing useful location information
- * about the error's position in the source.
- */
- public static function printError(Error $error): string
- {
- $printedLocations = [];
-
- $nodes = $error->nodes;
- if (isset($nodes) && $nodes !== []) {
- foreach ($nodes as $node) {
- $location = $node->loc;
- if (isset($location)) {
- $source = $location->source;
- if (isset($source)) {
- $printedLocations[] = self::highlightSourceAtLocation(
- $source,
- $source->getLocation($location->start)
- );
- }
- }
- }
- } elseif ($error->getSource() !== null && $error->getLocations() !== []) {
- $source = $error->getSource();
- foreach ($error->getLocations() as $location) {
- $printedLocations[] = self::highlightSourceAtLocation($source, $location);
- }
- }
-
- return $printedLocations === []
- ? $error->getMessage()
- : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
- }
-
- /**
- * Render a helpful description of the location of the error in the Automattic\WooCommerce\Vendor\GraphQL
- * Source document.
- */
- private static function highlightSourceAtLocation(Source $source, SourceLocation $location): string
- {
- $line = $location->line;
- $lineOffset = $source->locationOffset->line - 1;
- $columnOffset = self::getColumnOffset($source, $location);
- $contextLine = $line + $lineOffset;
- $contextColumn = $location->column + $columnOffset;
- $prevLineNum = (string) ($contextLine - 1);
- $lineNum = (string) $contextLine;
- $nextLineNum = (string) ($contextLine + 1);
- $padLen = strlen($nextLineNum);
-
- $lines = Utils::splitLines($source->body);
- $lines[0] = self::spaces($source->locationOffset->column - 1) . $lines[0];
-
- $outputLines = [
- "{$source->name} ({$contextLine}:{$contextColumn})",
- $line >= 2 ? (self::leftPad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
- self::leftPad($padLen, $lineNum) . ': ' . $lines[$line - 1],
- self::spaces(2 + $padLen + $contextColumn - 1) . '^',
- $line < count($lines) ? self::leftPad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
- ];
-
- return implode("\n", array_filter($outputLines));
- }
-
- private static function getColumnOffset(Source $source, SourceLocation $location): int
- {
- return $location->line === 1
- ? $source->locationOffset->column - 1
- : 0;
- }
-
- private static function spaces(int $length): string
- {
- return str_repeat(' ', $length);
- }
-
- private static function leftPad(int $length, string $str): string
- {
- return self::spaces($length - mb_strlen($str)) . $str;
- }
-
- /**
- * Convert any exception to a Automattic\WooCommerce\Vendor\GraphQL spec compliant array.
- *
- * This method only exposes the exception message when the given exception
- * implements the ClientAware interface, or when debug flags are passed.
- *
- * For a list of available debug flags @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag constants.
- *
- * @return SerializableError
- *
- * @api
- */
- public static function createFromException(\Throwable $exception, int $debugFlag = DebugFlag::NONE, ?string $internalErrorMessage = null): array
- {
- $internalErrorMessage ??= self::$internalErrorMessage;
-
- $message = $exception instanceof ClientAware && $exception->isClientSafe()
- ? $exception->getMessage()
- : $internalErrorMessage;
-
- $formattedError = ['message' => $message];
-
- if ($exception instanceof Error) {
- $locations = array_map(
- static fn (SourceLocation $loc): array => $loc->toSerializableArray(),
- $exception->getLocations()
- );
- if ($locations !== []) {
- $formattedError['locations'] = $locations;
- }
-
- if ($exception->path !== null && $exception->path !== []) {
- $formattedError['path'] = $exception->path;
- }
- }
-
- if ($exception instanceof ProvidesExtensions) {
- $extensions = $exception->getExtensions();
- if (is_array($extensions) && $extensions !== []) {
- $formattedError['extensions'] = $extensions;
- }
- }
-
- if ($debugFlag !== DebugFlag::NONE) {
- $formattedError = self::addDebugEntries($formattedError, $exception, $debugFlag);
- }
-
- return $formattedError;
- }
-
- /**
- * Decorates spec-compliant $formattedError with debug entries according to $debug flags.
- *
- * @param SerializableError $formattedError
- * @param int $debugFlag For available flags @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag
- *
- * @throws \Throwable
- *
- * @return SerializableError
- */
- public static function addDebugEntries(array $formattedError, \Throwable $e, int $debugFlag): array
- {
- if ($debugFlag === DebugFlag::NONE) {
- return $formattedError;
- }
-
- if (($debugFlag & DebugFlag::RETHROW_INTERNAL_EXCEPTIONS) !== 0) {
- if (! $e instanceof Error) {
- throw $e;
- }
-
- if ($e->getPrevious() !== null) {
- throw $e->getPrevious();
- }
- }
-
- $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
-
- if (($debugFlag & DebugFlag::RETHROW_UNSAFE_EXCEPTIONS) !== 0 && $isUnsafe && $e->getPrevious() !== null) {
- throw $e->getPrevious();
- }
-
- if (($debugFlag & DebugFlag::INCLUDE_DEBUG_MESSAGE) !== 0 && $isUnsafe) {
- $formattedError['extensions']['debugMessage'] = $e->getMessage();
- }
-
- if (($debugFlag & DebugFlag::INCLUDE_TRACE) !== 0) {
- $actualError = $e->getPrevious() ?? $e;
- if ($e instanceof \ErrorException || $e instanceof \Error) {
- $formattedError['extensions']['file'] = $e->getFile();
- $formattedError['extensions']['line'] = $e->getLine();
- } else {
- $formattedError['extensions']['file'] = $actualError->getFile();
- $formattedError['extensions']['line'] = $actualError->getLine();
- }
-
- $isTrivial = $e instanceof Error && $e->getPrevious() === null;
-
- if (! $isTrivial) {
- $formattedError['extensions']['trace'] = static::toSafeTrace($actualError);
- }
- }
-
- return $formattedError;
- }
-
- /**
- * Prepares final error formatter taking in account $debug flags.
- *
- * If initial formatter is not set, FormattedError::createFromException is used.
- *
- * @phpstan-param ErrorFormatter|null $formatter
- */
- public static function prepareFormatter(?callable $formatter, int $debug): callable
- {
- return $formatter === null
- ? static fn (\Throwable $e): array => static::createFromException($e, $debug)
- : static fn (\Throwable $e): array => static::addDebugEntries($formatter($e), $e, $debug);
- }
-
- /**
- * Returns error trace as serializable array.
- *
- * @return array<int, array{
- * file?: string,
- * line?: int,
- * function?: string,
- * call?: string,
- * }>
- *
- * @api
- */
- public static function toSafeTrace(\Throwable $error): array
- {
- $trace = $error->getTrace();
-
- if (
- isset($trace[0]['function']) && isset($trace[0]['class'])
- // Remove invariant entries as they don't provide much value:
- && ($trace[0]['class'] . '::' . $trace[0]['function'] === 'Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils::invariant')
- ) {
- array_shift($trace);
- } elseif (! isset($trace[0]['file'])) {
- // Remove root call as it's likely error handler trace:
- array_shift($trace);
- }
-
- $formatted = [];
- foreach ($trace as $err) {
- $safeErr = [];
-
- if (isset($err['file'])) {
- $safeErr['file'] = $err['file'];
- }
-
- if (isset($err['line'])) {
- $safeErr['line'] = $err['line'];
- }
-
- $func = $err['function'];
- $args = array_map([self::class, 'printVar'], $err['args'] ?? []);
- $funcStr = $func . '(' . implode(', ', $args) . ')';
-
- if (isset($err['class'])) {
- $safeErr['call'] = $err['class'] . '::' . $funcStr;
- } else {
- $safeErr['function'] = $funcStr;
- }
-
- $formatted[] = $safeErr;
- }
-
- return $formatted;
- }
-
- /** @param mixed $var */
- public static function printVar($var): string
- {
- if ($var instanceof Type) {
- return 'GraphQLType: ' . $var->toString();
- }
-
- if (is_object($var)) {
- // Calling `count` on instances of `PHPUnit\Framework\Test` triggers an unintended side effect - see https://github.com/sebastianbergmann/phpunit/issues/5866#issuecomment-2172429263
- $count = ! $var instanceof Test && $var instanceof \Countable
- ? '(' . count($var) . ')'
- : '';
-
- return 'instance of ' . get_class($var) . $count;
- }
-
- if (is_array($var)) {
- return 'array(' . count($var) . ')';
- }
-
- if ($var === '') {
- return '(empty string)';
- }
-
- if (is_string($var)) {
- return "'" . addcslashes($var, "'") . "'";
- }
-
- if (is_bool($var)) {
- return $var ? 'true' : 'false';
- }
-
- if (is_scalar($var)) {
- return (string) $var;
- }
-
- if ($var === null) {
- return 'null';
- }
-
- return gettype($var);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php b/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php
deleted file mode 100644
index c26260662e2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Note:
- * This exception should not inherit base Error exception as it is raised when there is an error somewhere in
- * user-land code.
- */
-class InvariantViolation extends \LogicException {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php b/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php
deleted file mode 100644
index b21ed004bb1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Implementing HasExtensions allows this error to provide additional data to clients.
- */
-interface ProvidesExtensions
-{
- /**
- * Data to include within the "extensions" key of the formatted error.
- *
- * @return array<string, mixed>|null
- */
- public function getExtensions(): ?array;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php
deleted file mode 100644
index 8b6d4880edb..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Thrown when failing to serialize a leaf value.
- *
- * Not generally safe for clients, as the wrong given value could
- * be something not intended to ever be seen by clients.
- */
-class SerializationError extends \Exception {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php
deleted file mode 100644
index b44e94afa42..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-
-class SyntaxError extends Error
-{
- public function __construct(Source $source, int $position, string $description)
- {
- parent::__construct(
- "Syntax Error: {$description}",
- null,
- $source,
- [$position]
- );
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php
deleted file mode 100644
index e7ba3ecc47b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Caused by Automattic\WooCommerce\Vendor\GraphQL clients and can safely be displayed.
- */
-class UserError extends \RuntimeException implements ClientAware
-{
- public function isClientSafe(): bool
- {
- return true;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php b/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php
deleted file mode 100644
index 38b21f72e9f..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
-
-/**
- * Encapsulates warnings produced by the library.
- *
- * Warnings can be suppressed (individually or all) if required.
- * Also, it is possible to override warning handler (which is **trigger_error()** by default).
- *
- * @phpstan-type WarningHandler callable(string $errorMessage, int $warningId, ?int $messageLevel): void
- */
-final class Warning
-{
- public const NONE = 0;
- public const WARNING_ASSIGN = 2;
- public const WARNING_CONFIG = 4;
- public const WARNING_FULL_SCHEMA_SCAN = 8;
- public const WARNING_CONFIG_DEPRECATION = 16;
- public const WARNING_NOT_A_TYPE = 32;
- public const ALL = 63;
-
- private static int $enableWarnings = self::ALL;
-
- /** @var array<int, true> */
- private static array $warned = [];
-
- /**
- * @var callable|null
- *
- * @phpstan-var WarningHandler|null
- */
- private static $warningHandler;
-
- /**
- * Sets warning handler which can intercept all system warnings.
- * When not set, trigger_error() is used to notify about warnings.
- *
- * @phpstan-param WarningHandler|null $warningHandler
- *
- * @api
- */
- public static function setWarningHandler(?callable $warningHandler = null): void
- {
- self::$warningHandler = $warningHandler;
- }
-
- /**
- * Suppress warning by id (has no effect when custom warning handler is set).
- *
- * @param bool|int $suppress
- *
- * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) suppress a specific warning
- * @example Warning::suppress(true) suppresses all warnings
- * @example Warning::suppress(false) enables all warnings
- *
- * @api
- */
- public static function suppress($suppress = true): void
- {
- if ($suppress === true) {
- self::$enableWarnings = 0;
- } elseif ($suppress === false) {
- self::$enableWarnings = self::ALL;
- // @phpstan-ignore-next-line necessary until we can use proper unions
- } elseif (is_int($suppress)) {
- self::$enableWarnings &= ~$suppress;
- } else {
- $type = gettype($suppress);
- throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
- }
- }
-
- /**
- * Re-enable previously suppressed warning by id (has no effect when custom warning handler is set).
- *
- * @param bool|int $enable
- *
- * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) re-enables a specific warning
- * @example Warning::suppress(true) re-enables all warnings
- * @example Warning::suppress(false) suppresses all warnings
- *
- * @api
- */
- public static function enable($enable = true): void
- {
- if ($enable === true) {
- self::$enableWarnings = self::ALL;
- } elseif ($enable === false) {
- self::$enableWarnings = 0;
- // @phpstan-ignore-next-line necessary until we can use proper unions
- } elseif (is_int($enable)) {
- self::$enableWarnings |= $enable;
- } else {
- $type = gettype($enable);
- throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
- }
- }
-
- public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null): void
- {
- $messageLevel ??= \E_USER_WARNING;
-
- if (self::$warningHandler !== null) {
- (self::$warningHandler)($errorMessage, $warningId, $messageLevel);
- } elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
- self::$warned[$warningId] = true;
- trigger_error($errorMessage, $messageLevel);
- }
- }
-
- public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null): void
- {
- $messageLevel ??= \E_USER_WARNING;
-
- if (self::$warningHandler !== null) {
- (self::$warningHandler)($errorMessage, $warningId, $messageLevel);
- } elseif ((self::$enableWarnings & $warningId) > 0) {
- trigger_error($errorMessage, $messageLevel);
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php
deleted file mode 100644
index 1a18dfcc52a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * Data that must be available at all points during query execution.
- *
- * Namely, schema of the type system that is currently executing,
- * and the fragments defined in the query document.
- *
- * @phpstan-import-type FieldResolver from Executor
- * @phpstan-import-type ArgsMapper from Executor
- */
-class ExecutionContext
-{
- public Schema $schema;
-
- /** @var array<string, FragmentDefinitionNode> */
- public array $fragments;
-
- /** @var mixed */
- public $rootValue;
-
- /** @var mixed */
- public $contextValue;
-
- public OperationDefinitionNode $operation;
-
- /** @var array<string, mixed> */
- public array $variableValues;
-
- /**
- * @var callable
- *
- * @phpstan-var FieldResolver
- */
- public $fieldResolver;
-
- /**
- * @var callable
- *
- * @phpstan-var ArgsMapper
- */
- public $argsMapper;
-
- /** @var list<Error> */
- public array $errors;
-
- public PromiseAdapter $promiseAdapter;
-
- /**
- * @param array<string, FragmentDefinitionNode> $fragments
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed> $variableValues
- * @param list<Error> $errors
- *
- * @phpstan-param FieldResolver $fieldResolver
- */
- public function __construct(
- Schema $schema,
- array $fragments,
- $rootValue,
- $contextValue,
- OperationDefinitionNode $operation,
- array $variableValues,
- array $errors,
- callable $fieldResolver,
- callable $argsMapper,
- PromiseAdapter $promiseAdapter
- ) {
- $this->schema = $schema;
- $this->fragments = $fragments;
- $this->rootValue = $rootValue;
- $this->contextValue = $contextValue;
- $this->operation = $operation;
- $this->variableValues = $variableValues;
- $this->errors = $errors;
- $this->fieldResolver = $fieldResolver;
- $this->argsMapper = $argsMapper;
- $this->promiseAdapter = $promiseAdapter;
- }
-
- public function addError(Error $error): void
- {
- $this->errors[] = $error;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php
deleted file mode 100644
index 451f0c5ad7e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError;
-
-/**
- * Returned after [query execution](executing-queries.md).
- * Represents both - result of successful execution and of a failed one
- * (with errors collected in `errors` prop).
- *
- * Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format)
- * serializable array using `toArray()`.
- *
- * @phpstan-type SerializableError array{
- * message: string,
- * locations?: array<int, array{line: int, column: int}>,
- * path?: array<int, int|string>,
- * extensions?: array<string, mixed>
- * }
- * @phpstan-type SerializableErrors list<SerializableError>
- * @phpstan-type SerializableResult array{
- * data?: array<string, mixed>,
- * errors?: SerializableErrors,
- * extensions?: array<string, mixed>
- * }
- * @phpstan-type ErrorFormatter callable(\Throwable): SerializableError
- * @phpstan-type ErrorsHandler callable(list<Error> $errors, ErrorFormatter $formatter): SerializableErrors
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ExecutionResultTest
- */
-class ExecutionResult implements \JsonSerializable
-{
- /**
- * Data collected from resolvers during query execution.
- *
- * @api
- *
- * @var array<string, mixed>|null
- */
- public ?array $data = null;
-
- /**
- * Errors registered during query execution.
- *
- * If an error was caused by exception thrown in resolver, $error->getPrevious() would
- * contain original exception.
- *
- * @api
- *
- * @var list<Error>
- */
- public array $errors = [];
-
- /**
- * User-defined serializable array of extensions included in serialized result.
- *
- * @api
- *
- * @var array<string, mixed>|null
- */
- public ?array $extensions = null;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ErrorFormatter|null
- */
- private $errorFormatter;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ErrorsHandler|null
- */
- private $errorsHandler;
-
- /**
- * @param array<string, mixed>|null $data
- * @param list<Error> $errors
- * @param array<string, mixed> $extensions
- */
- public function __construct(?array $data = null, array $errors = [], array $extensions = [])
- {
- $this->data = $data;
- $this->errors = $errors;
- $this->extensions = $extensions;
- }
-
- /**
- * Define custom error formatting (must conform to http://facebook.github.io/graphql/#sec-Errors).
- *
- * Expected signature is: function (Automattic\WooCommerce\Vendor\GraphQL\Error\Error $error): array
- *
- * Default formatter is "Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError::createFromException"
- *
- * Expected returned value must be an array:
- * array(
- * 'message' => 'errorMessage',
- * // ... other keys
- * );
- *
- * @phpstan-param ErrorFormatter|null $errorFormatter
- *
- * @api
- */
- public function setErrorFormatter(?callable $errorFormatter): self
- {
- $this->errorFormatter = $errorFormatter;
-
- return $this;
- }
-
- /**
- * Define custom logic for error handling (filtering, logging, etc).
- *
- * Expected handler signature is:
- * fn (array $errors, callable $formatter): array
- *
- * Default handler is:
- * fn (array $errors, callable $formatter): array => array_map($formatter, $errors)
- *
- * @phpstan-param ErrorsHandler|null $errorsHandler
- *
- * @api
- */
- public function setErrorsHandler(?callable $errorsHandler): self
- {
- $this->errorsHandler = $errorsHandler;
-
- return $this;
- }
-
- /** @phpstan-return SerializableResult */
- #[\ReturnTypeWillChange]
- public function jsonSerialize(): array
- {
- return $this->toArray();
- }
-
- /**
- * Converts Automattic\WooCommerce\Vendor\GraphQL query result to spec-compliant serializable array using provided
- * errors handler and formatter.
- *
- * If debug argument is passed, output of error formatter is enriched which debugging information
- * ("debugMessage", "trace" keys depending on flags).
- *
- * $debug argument must sum of flags from @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag
- *
- * @phpstan-return SerializableResult
- *
- * @api
- */
- public function toArray(int $debug = DebugFlag::NONE): array
- {
- $result = [];
-
- if ($this->errors !== []) {
- $errorsHandler = $this->errorsHandler
- ?? static fn (array $errors, callable $formatter): array => array_map($formatter, $errors);
-
- /** @phpstan-var SerializableErrors */
- $handledErrors = $errorsHandler(
- $this->errors,
- FormattedError::prepareFormatter($this->errorFormatter, $debug)
- );
-
- // While we know that there were errors initially, they might have been discarded
- if ($handledErrors !== []) {
- $result['errors'] = $handledErrors;
- }
- }
-
- if ($this->data !== null) {
- $result['data'] = $this->data;
- }
-
- if ($this->extensions !== null && $this->extensions !== []) {
- $result['extensions'] = $this->extensions;
- }
-
- return $result;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php
deleted file mode 100644
index 0783f1c7388..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php
+++ /dev/null
@@ -1,219 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Implements the "Evaluating requests" section of the Automattic\WooCommerce\Vendor\GraphQL specification.
- *
- * @phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed
- * @phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed
- * @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ExecutorTest
- */
-class Executor
-{
- /**
- * @var callable
- *
- * @phpstan-var FieldResolver
- */
- private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
-
- /**
- * @var callable
- *
- * @phpstan-var ArgsMapper
- */
- private static $defaultArgsMapper = [self::class, 'defaultArgsMapper'];
-
- private static ?PromiseAdapter $defaultPromiseAdapter;
-
- /**
- * @var callable
- *
- * @phpstan-var ImplementationFactory
- */
- private static $implementationFactory = [ReferenceExecutor::class, 'create'];
-
- /** @phpstan-return FieldResolver */
- public static function getDefaultFieldResolver(): callable
- {
- return self::$defaultFieldResolver;
- }
-
- /**
- * Set a custom default resolve function.
- *
- * @phpstan-param FieldResolver $fieldResolver
- */
- public static function setDefaultFieldResolver(callable $fieldResolver): void
- {
- self::$defaultFieldResolver = $fieldResolver;
- }
-
- /** @phpstan-return ArgsMapper */
- public static function getDefaultArgsMapper(): callable
- {
- return self::$defaultArgsMapper;
- }
-
- /** @phpstan-param ArgsMapper $argsMapper */
- public static function setDefaultArgsMapper(callable $argsMapper): void
- {
- self::$defaultArgsMapper = $argsMapper;
- }
-
- public static function getDefaultPromiseAdapter(): PromiseAdapter
- {
- return self::$defaultPromiseAdapter ??= new SyncPromiseAdapter();
- }
-
- /** Set a custom default promise adapter. */
- public static function setDefaultPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null): void
- {
- self::$defaultPromiseAdapter = $defaultPromiseAdapter;
- }
-
- /** @phpstan-return ImplementationFactory */
- public static function getImplementationFactory(): callable
- {
- return self::$implementationFactory;
- }
-
- /**
- * Set a custom executor implementation factory.
- *
- * @phpstan-param ImplementationFactory $implementationFactory
- */
- public static function setImplementationFactory(callable $implementationFactory): void
- {
- self::$implementationFactory = $implementationFactory;
- }
-
- /**
- * Executes DocumentNode against given $schema.
- *
- * Always returns ExecutionResult and never throws.
- * All errors which occur during operation execution are collected in `$result->errors`.
- *
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed>|null $variableValues
- *
- * @phpstan-param FieldResolver|null $fieldResolver
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public static function execute(
- Schema $schema,
- DocumentNode $documentNode,
- $rootValue = null,
- $contextValue = null,
- ?array $variableValues = null,
- ?string $operationName = null,
- ?callable $fieldResolver = null
- ): ExecutionResult {
- $promiseAdapter = new SyncPromiseAdapter();
-
- $result = static::promiseToExecute(
- $promiseAdapter,
- $schema,
- $documentNode,
- $rootValue,
- $contextValue,
- $variableValues,
- $operationName,
- $fieldResolver
- );
-
- return $promiseAdapter->wait($result);
- }
-
- /**
- * Same as execute(), but requires promise adapter and returns a promise which is always
- * fulfilled with an instance of ExecutionResult and never rejected.
- *
- * Useful for async PHP platforms.
- *
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed>|null $variableValues
- *
- * @phpstan-param FieldResolver|null $fieldResolver
- * @phpstan-param ArgsMapper|null $argsMapper
- *
- * @api
- */
- public static function promiseToExecute(
- PromiseAdapter $promiseAdapter,
- Schema $schema,
- DocumentNode $documentNode,
- $rootValue = null,
- $contextValue = null,
- ?array $variableValues = null,
- ?string $operationName = null,
- ?callable $fieldResolver = null,
- ?callable $argsMapper = null
- ): Promise {
- $executor = (self::$implementationFactory)(
- $promiseAdapter,
- $schema,
- $documentNode,
- $rootValue,
- $contextValue,
- $variableValues ?? [],
- $operationName,
- $fieldResolver ?? self::$defaultFieldResolver,
- $argsMapper ?? self::$defaultArgsMapper,
- );
-
- return $executor->doExecute();
- }
-
- /**
- * If a resolve function is not given, then a default resolve behavior is used
- * which takes the property of the root value of the same name as the field
- * and returns it as the result, or if it's a function, returns the result
- * of calling that function while passing along args and context.
- *
- * @param mixed $objectLikeValue
- * @param array<string, mixed> $args
- * @param mixed $contextValue
- *
- * @return mixed
- */
- public static function defaultFieldResolver($objectLikeValue, array $args, $contextValue, ResolveInfo $info)
- {
- $property = Utils::extractKey($objectLikeValue, $info->fieldName);
-
- return $property instanceof \Closure
- ? $property($objectLikeValue, $args, $contextValue, $info)
- : $property;
- }
-
- /**
- * @template T of array<string, mixed>
- *
- * @param T $args
- *
- * @return T
- */
- public static function defaultArgsMapper(array $args): array
- {
- return $args;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php
deleted file mode 100644
index 16dd09e116b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-
-interface ExecutorImplementation
-{
- /** Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject. */
- public function doExecute(): Promise;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php
deleted file mode 100644
index 782aeda42a8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php
+++ /dev/null
@@ -1,181 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-use Amp\DeferredFuture;
-use Amp\Future;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-
-use function Amp\async;
-use function Amp\Future\await;
-
-/**
- * Allows integration with amphp/amp v3 (fiber-based futures).
- *
- * @see https://amphp.org/amp
- */
-class AmpFutureAdapter implements PromiseAdapter
-{
- public function isThenable($value): bool
- {
- return $value instanceof Future;
- }
-
- /** @throws InvariantViolation */
- public function convertThenable($thenable): Promise
- {
- return new Promise($thenable, $this);
- }
-
- /** @throws InvariantViolation */
- public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
- {
- $future = $promise->adoptedPromise;
- assert($future instanceof Future);
-
- $next = async(static function () use ($future, $onFulfilled, $onRejected) {
- try {
- $value = $future->await();
- } catch (\Throwable $reason) {
- if ($onRejected === null) {
- throw $reason;
- }
-
- return static::unwrapResult($onRejected($reason));
- }
-
- if ($onFulfilled === null) {
- return $value;
- }
-
- return static::unwrapResult($onFulfilled($value));
- });
-
- return new Promise($next, $this);
- }
-
- /** @throws InvariantViolation */
- public function create(callable $resolver): Promise
- {
- $deferred = new DeferredFuture();
-
- try {
- $resolver(
- static function ($value) use ($deferred): void {
- static::resolveDeferred($deferred, $value);
- },
- static function (\Throwable $exception) use ($deferred): void {
- $deferred->error($exception);
- }
- );
- } catch (\Throwable $exception) {
- $deferred->error($exception);
- }
-
- return new Promise($deferred->getFuture(), $this);
- }
-
- /**
- * @throws \Error
- * @throws InvariantViolation
- */
- public function createFulfilled($value = null): Promise
- {
- if ($value instanceof Promise) {
- return $value;
- }
-
- if ($value instanceof Future) {
- return new Promise($value, $this);
- }
-
- return new Promise(Future::complete($value), $this);
- }
-
- /** @throws InvariantViolation */
- public function createRejected(\Throwable $reason): Promise
- {
- return new Promise(Future::error($reason), $this);
- }
-
- /**
- * @throws \Error
- * @throws InvariantViolation
- */
- public function all(iterable $promisesOrValues): Promise
- {
- $items = is_array($promisesOrValues)
- ? $promisesOrValues
- : iterator_to_array($promisesOrValues);
-
- /** @var array<Future<mixed>> $futures */
- $futures = [];
-
- foreach ($items as $key => $item) {
- if ($item instanceof Promise) {
- $item = $item->adoptedPromise;
- }
-
- if ($item instanceof Future) {
- $futures[$key] = $item;
- }
- }
-
- $combined = async(static function () use ($items, $futures): array {
- if ($futures === []) {
- return $items;
- }
-
- $resolved = await($futures);
-
- return array_replace($items, $resolved);
- });
-
- return new Promise($combined, $this);
- }
-
- /**
- * @param DeferredFuture<mixed> $deferred
- * @param mixed $value
- */
- protected static function resolveDeferred(DeferredFuture $deferred, $value): void
- {
- if ($value instanceof Promise) {
- $value = $value->adoptedPromise;
- }
-
- if ($value instanceof Future) {
- async(static function () use ($deferred, $value): void {
- try {
- $deferred->complete($value->await());
- } catch (\Throwable $exception) {
- $deferred->error($exception);
- }
- });
-
- return;
- }
-
- $deferred->complete($value);
- }
-
- /**
- * @param mixed $value
- *
- * @return mixed
- */
- protected static function unwrapResult($value)
- {
- if ($value instanceof Promise) {
- $value = $value->adoptedPromise;
- }
-
- if ($value instanceof Future) {
- return $value->await();
- }
-
- return $value;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php
deleted file mode 100644
index 35d03aaf266..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-use Amp\Deferred;
-use Amp\Failure;
-use Amp\Promise as AmpPromise;
-use Amp\Success;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-
-use function Amp\Promise\all;
-
-class AmpPromiseAdapter implements PromiseAdapter
-{
- public function isThenable($value): bool
- {
- return $value instanceof AmpPromise;
- }
-
- /** @throws InvariantViolation */
- public function convertThenable($thenable): Promise
- {
- return new Promise($thenable, $this);
- }
-
- /** @throws InvariantViolation */
- public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
- {
- $deferred = new Deferred();
- $onResolve = static function (?\Throwable $reason, $value) use ($onFulfilled, $onRejected, $deferred): void {
- if ($reason === null && $onFulfilled !== null) {
- self::resolveWithCallable($deferred, $onFulfilled, $value);
- } elseif ($reason === null) {
- $deferred->resolve($value);
- } elseif ($onRejected !== null) {
- self::resolveWithCallable($deferred, $onRejected, $reason);
- } else {
- $deferred->fail($reason);
- }
- };
-
- $ampPromise = $promise->adoptedPromise;
- assert($ampPromise instanceof AmpPromise);
- $ampPromise->onResolve($onResolve);
-
- return new Promise($deferred->promise(), $this);
- }
-
- /** @throws InvariantViolation */
- public function create(callable $resolver): Promise
- {
- $deferred = new Deferred();
-
- $resolver(
- static function ($value) use ($deferred): void {
- $deferred->resolve($value);
- },
- static function (\Throwable $exception) use ($deferred): void {
- $deferred->fail($exception);
- }
- );
-
- return new Promise($deferred->promise(), $this);
- }
-
- /**
- * @throws \Error
- * @throws InvariantViolation
- */
- public function createFulfilled($value = null): Promise
- {
- $promise = new Success($value);
-
- return new Promise($promise, $this);
- }
-
- /** @throws InvariantViolation */
- public function createRejected(\Throwable $reason): Promise
- {
- $promise = new Failure($reason);
-
- return new Promise($promise, $this);
- }
-
- /**
- * @throws \Error
- * @throws InvariantViolation
- */
- public function all(iterable $promisesOrValues): Promise
- {
- /** @var array<AmpPromise<mixed>> $promises */
- $promises = [];
- foreach ($promisesOrValues as $key => $item) {
- if ($item instanceof Promise) {
- $ampPromise = $item->adoptedPromise;
- assert($ampPromise instanceof AmpPromise);
- $promises[$key] = $ampPromise;
- } elseif ($item instanceof AmpPromise) {
- $promises[$key] = $item;
- }
- }
-
- $deferred = new Deferred();
-
- all($promises)->onResolve(static function (?\Throwable $reason, ?array $values) use ($promisesOrValues, $deferred): void {
- if ($reason === null) {
- assert(is_array($values), 'Either $reason or $values must be passed');
-
- $promisesOrValuesArray = is_array($promisesOrValues)
- ? $promisesOrValues
- : iterator_to_array($promisesOrValues);
- $resolvedValues = array_replace($promisesOrValuesArray, $values);
- $deferred->resolve($resolvedValues);
-
- return;
- }
-
- $deferred->fail($reason);
- });
-
- return new Promise($deferred->promise(), $this);
- }
-
- /**
- * @template TArgument
- * @template TResult of AmpPromise<mixed>
- *
- * @param Deferred<TResult> $deferred
- * @param callable(TArgument): TResult $callback
- * @param TArgument $argument
- */
- private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument): void
- {
- try {
- $result = $callback($argument);
- } catch (\Throwable $exception) {
- $deferred->fail($exception);
-
- return;
- }
-
- if ($result instanceof Promise) {
- /** @var TResult $result */
- $result = $result->adoptedPromise;
- }
-
- $deferred->resolve($result);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php
deleted file mode 100644
index 4db5e2ccadf..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use React\Promise\Promise as ReactPromise;
-use React\Promise\PromiseInterface as ReactPromiseInterface;
-
-use function React\Promise\all;
-use function React\Promise\reject;
-use function React\Promise\resolve;
-
-class ReactPromiseAdapter implements PromiseAdapter
-{
- public function isThenable($value): bool
- {
- return $value instanceof ReactPromiseInterface;
- }
-
- /** @throws InvariantViolation */
- public function convertThenable($thenable): Promise
- {
- return new Promise($thenable, $this);
- }
-
- /** @throws InvariantViolation */
- public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
- {
- $reactPromise = $promise->adoptedPromise;
- assert($reactPromise instanceof ReactPromiseInterface);
-
- return new Promise($reactPromise->then($onFulfilled, $onRejected), $this);
- }
-
- /** @throws InvariantViolation */
- public function create(callable $resolver): Promise
- {
- $reactPromise = new ReactPromise($resolver);
-
- return new Promise($reactPromise, $this);
- }
-
- /** @throws InvariantViolation */
- public function createFulfilled($value = null): Promise
- {
- $reactPromise = resolve($value);
-
- return new Promise($reactPromise, $this);
- }
-
- /** @throws InvariantViolation */
- public function createRejected(\Throwable $reason): Promise
- {
- $reactPromise = reject($reason);
-
- return new Promise($reactPromise, $this);
- }
-
- /** @throws InvariantViolation */
- public function all(iterable $promisesOrValues): Promise
- {
- foreach ($promisesOrValues as &$promiseOrValue) {
- if ($promiseOrValue instanceof Promise) {
- $promiseOrValue = $promiseOrValue->adoptedPromise;
- }
- }
-
- $promisesOrValuesArray = is_array($promisesOrValues)
- ? $promisesOrValues
- : iterator_to_array($promisesOrValues);
- $reactPromise = all($promisesOrValuesArray)->then(static fn (array $values): array => array_map(
- static fn ($key) => $values[$key],
- array_keys($promisesOrValuesArray),
- ));
-
- return new Promise($reactPromise, $this);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php
deleted file mode 100644
index b1c039e47c8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php
+++ /dev/null
@@ -1,212 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-
-/**
- * Synchronous promise implementation following Promises A+ spec.
- *
- * Uses a hybrid approach for optimal memory and performance:
- * - Lightweight closures in queue (fast execution)
- * - Heavy payload (callbacks) stored on promise objects and cleared after use
- *
- * Library users should use @see \Automattic\WooCommerce\Vendor\GraphQL\Deferred to create promises.
- *
- * @phpstan-type Executor callable(): mixed
- */
-class SyncPromise
-{
- /**
- * TODO remove in next major version.
- *
- * @deprecated Use SyncPromiseQueue::run() instead
- */
- public static function runQueue(): void
- {
- SyncPromiseQueue::run();
- }
-
- /**
- * TODO remove in next major version.
- *
- * @deprecated Use SyncPromiseQueue methods instead
- *
- * @return \SplQueue<callable(): void>
- */
- public static function getQueue(): \SplQueue
- {
- return SyncPromiseQueue::queue();
- }
-
- public const PENDING = 0;
- public const FULFILLED = 1;
- public const REJECTED = 2;
-
- /**
- * Current promise state.
- *
- * @var 0|1|2
- */
- public int $state = self::PENDING;
-
- /**
- * Resolved value or rejection reason.
- *
- * @var mixed
- */
- public $result;
-
- /**
- * Promises created in `then` method awaiting resolution.
- *
- * @var array<
- * int,
- * array{
- * self,
- * (callable(mixed): mixed)|null,
- * (callable(\Throwable): mixed)|null,
- * },
- * >
- */
- protected array $waiting = [];
-
- /**
- * @param mixed $value
- *
- * @throws \Exception
- */
- public function resolve($value): self
- {
- switch ($this->state) {
- case self::PENDING:
- if ($value === $this) {
- throw new \Exception('Cannot resolve promise with self.');
- }
-
- if (is_object($value) && method_exists($value, 'then')) {
- $value->then(
- function ($resolvedValue): void {
- $this->resolve($resolvedValue);
- },
- function (\Throwable $reason): void {
- $this->reject($reason);
- }
- );
-
- return $this;
- }
-
- $this->state = self::FULFILLED;
- $this->result = $value;
- $this->enqueueWaitingPromises();
- break;
- case self::FULFILLED:
- if ($this->result !== $value) {
- throw new \Exception('Cannot change value of fulfilled promise.');
- }
-
- break;
- case self::REJECTED:
- throw new \Exception('Cannot resolve rejected promise.');
- }
-
- return $this;
- }
-
- /**
- * @throws \Exception
- *
- * @return $this
- */
- public function reject(\Throwable $reason): self
- {
- switch ($this->state) {
- case self::PENDING:
- $this->state = self::REJECTED;
- $this->result = $reason;
- $this->enqueueWaitingPromises();
- break;
- case self::REJECTED:
- if ($reason !== $this->result) {
- throw new \Exception('Cannot change rejection reason.');
- }
-
- break;
- case self::FULFILLED:
- throw new \Exception('Cannot reject fulfilled promise.');
- }
-
- return $this;
- }
-
- /**
- * @param (callable(mixed): mixed)|null $onFulfilled
- * @param (callable(\Throwable): mixed)|null $onRejected
- *
- * @throws InvariantViolation
- */
- public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
- {
- if ($this->state === self::REJECTED
- && $onRejected === null
- ) {
- return $this;
- }
-
- if ($this->state === self::FULFILLED
- && $onFulfilled === null
- ) {
- return $this;
- }
-
- $child = new self();
-
- $this->waiting[] = [$child, $onFulfilled, $onRejected];
-
- if ($this->state !== self::PENDING) {
- $this->enqueueWaitingPromises();
- }
-
- return $child;
- }
-
- /** @throws InvariantViolation */
- private function enqueueWaitingPromises(): void
- {
- if ($this->state === self::PENDING) {
- throw new InvariantViolation('Cannot enqueue derived promises when parent is still pending.');
- }
-
- $waiting = $this->waiting;
- if ($waiting === []) {
- return;
- }
-
- $this->waiting = [];
-
- $result = $this->result;
-
- SyncPromiseQueue::enqueue(static function () use ($waiting, $result): void {
- foreach ($waiting as [$child, $onFulfilled, $onRejected]) {
- try {
- if ($result instanceof \Throwable) {
- if ($onRejected === null) {
- $child->reject($result);
- } else {
- $child->resolve($onRejected($result));
- }
- } else {
- $child->resolve(
- $onFulfilled === null
- ? $result
- : $onFulfilled($result)
- );
- }
- } catch (\Throwable $e) {
- $child->reject($e);
- }
- }
- });
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php
deleted file mode 100644
index 44ff4f56342..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Allows changing order of field resolution even in sync environments
- * (by leveraging queue of deferreds and promises).
- */
-class SyncPromiseAdapter implements PromiseAdapter
-{
- public function isThenable($value): bool
- {
- return $value instanceof SyncPromise;
- }
-
- /** @throws InvariantViolation */
- public function convertThenable($thenable): Promise
- {
- if (! $thenable instanceof SyncPromise) {
- // End-users should always use Deferred, not SyncPromise directly
- $deferredClass = Deferred::class;
- $safeThenable = Utils::printSafe($thenable);
- throw new InvariantViolation("Expected instance of {$deferredClass}, got {$safeThenable}.");
- }
-
- return new Promise($thenable, $this);
- }
-
- /** @throws InvariantViolation */
- public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
- {
- $syncPromise = $promise->adoptedPromise;
- assert($syncPromise instanceof SyncPromise);
-
- return new Promise($syncPromise->then($onFulfilled, $onRejected), $this);
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function create(callable $resolver): Promise
- {
- $syncPromise = new SyncPromise();
-
- try {
- $resolver(
- [$syncPromise, 'resolve'],
- [$syncPromise, 'reject']
- );
- } catch (\Throwable $e) {
- $syncPromise->reject($e);
- }
-
- return new Promise($syncPromise, $this);
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function createFulfilled($value = null): Promise
- {
- $syncPromise = new SyncPromise();
-
- return new Promise($syncPromise->resolve($value), $this);
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function createRejected(\Throwable $reason): Promise
- {
- $syncPromise = new SyncPromise();
-
- return new Promise($syncPromise->reject($reason), $this);
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function all(iterable $promisesOrValues): Promise
- {
- $all = new SyncPromise();
-
- $total = is_array($promisesOrValues)
- ? count($promisesOrValues)
- : iterator_count($promisesOrValues);
- $count = 0;
- $result = [];
-
- $resolveAllWhenFinished = function () use (&$count, &$total, $all, &$result): void {
- if ($count === $total) {
- $all->resolve($result);
- }
- };
-
- foreach ($promisesOrValues as $index => $promiseOrValue) {
- if ($promiseOrValue instanceof Promise) {
- $result[$index] = null;
- $promiseOrValue->then(
- static function ($value) use (&$result, $index, &$count, &$resolveAllWhenFinished): void {
- $result[$index] = $value;
- ++$count;
- $resolveAllWhenFinished();
- },
- [$all, 'reject']
- );
- continue;
- }
-
- $result[$index] = $promiseOrValue;
- ++$count;
- }
-
- $resolveAllWhenFinished();
-
- return new Promise($all, $this);
- }
-
- /**
- * Synchronously wait when promise completes.
- *
- * @throws InvariantViolation
- *
- * @return mixed
- */
- public function wait(Promise $promise)
- {
- $this->beforeWait($promise);
-
- $syncPromise = $promise->adoptedPromise;
- assert($syncPromise instanceof SyncPromise);
-
- while ($syncPromise->state === SyncPromise::PENDING) {
- SyncPromiseQueue::run();
- $this->onWait($promise);
- }
-
- if ($syncPromise->state === SyncPromise::FULFILLED) {
- return $syncPromise->result;
- }
-
- if ($syncPromise->state === SyncPromise::REJECTED) {
- throw $syncPromise->result;
- }
-
- throw new InvariantViolation('Could not resolve promise.');
- }
-
- /** Execute just before starting to run promise completion. */
- protected function beforeWait(Promise $promise): void {}
-
- /** Execute while running promise completion. */
- protected function onWait(Promise $promise): void {}
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php
deleted file mode 100644
index 3d0cbbe4a8d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
-
-/**
- * Queue for deferred execution of SyncPromise tasks.
- *
- * Owns the shared queue and provides the run loop for processing promises.
- *
- * @api
- *
- * @phpstan-type Task callable(): void
- */
-class SyncPromiseQueue
-{
- /**
- * Adds a task to the queue.
- *
- * @param Task $task
- *
- * @api
- */
- public static function enqueue(callable $task): void
- {
- self::queue()->enqueue($task);
- }
-
- /**
- * Process all queued promises until the queue is empty.
- *
- * @api
- */
- public static function run(): void
- {
- $queue = self::queue();
- while (! $queue->isEmpty()) {
- $task = $queue->dequeue();
- $task();
- }
- }
-
- /**
- * Check if the queue is empty.
- *
- * @api
- */
- public static function isEmpty(): bool
- {
- return self::queue()->isEmpty();
- }
-
- /**
- * Return the number of tasks in the queue.
- *
- * @api
- */
- public static function count(): int
- {
- return self::queue()->count();
- }
-
- /**
- * TODO change to protected in next major version.
- *
- * @return \SplQueue<Task>
- */
- public static function queue(): \SplQueue
- {
- /** @var \SplQueue<Task>|null $queue */
- static $queue;
-
- return $queue ??= new \SplQueue();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php
deleted file mode 100644
index cce025f2ab6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise;
-
-use Amp\Future as AmpFuture;
-use Amp\Promise as AmpPromise;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromise;
-use React\Promise\PromiseInterface as ReactPromise;
-
-/**
- * Convenience wrapper for promises represented by Promise Adapter.
- */
-class Promise
-{
- /** @var SyncPromise|ReactPromise<mixed>|AmpFuture<mixed>|AmpPromise<mixed> */
- public $adoptedPromise;
-
- private PromiseAdapter $adapter;
-
- /**
- * @param mixed $adoptedPromise
- *
- * @throws InvariantViolation
- */
- public function __construct($adoptedPromise, PromiseAdapter $adapter)
- {
- if ($adoptedPromise instanceof self) {
- $selfClass = self::class;
- throw new InvariantViolation("Expected promise from adapted system, got {$selfClass}.");
- }
-
- $this->adoptedPromise = $adoptedPromise;
- $this->adapter = $adapter;
- }
-
- public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise
- {
- return $this->adapter->then($this, $onFulfilled, $onRejected);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php
deleted file mode 100644
index ae331ad10e0..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise;
-
-/**
- * Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)).
- */
-interface PromiseAdapter
-{
- /**
- * Is the value a promise or a deferred of the underlying platform?
- *
- * @param mixed $value
- *
- * @api
- */
- public function isThenable($value): bool;
-
- /**
- * Converts thenable of the underlying platform into Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise instance.
- *
- * @param mixed $thenable
- *
- * @api
- */
- public function convertThenable($thenable): Promise;
-
- /**
- * Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
- * in Promises/A+ specs. Then returns new wrapped instance of Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise.
- *
- * @api
- */
- public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise;
-
- /**
- * Creates a Promise from the given resolver callable.
- *
- * @param callable(callable $resolve, callable $reject): void $resolver
- *
- * @api
- */
- public function create(callable $resolver): Promise;
-
- /**
- * Creates a fulfilled Promise for a value if the value is not a promise.
- *
- * @param mixed $value
- *
- * @api
- */
- public function createFulfilled($value = null): Promise;
-
- /**
- * Creates a rejected promise for a reason if the reason is not a promise.
- *
- * If the provided reason is a promise, then it is returned as-is.
- *
- * @api
- */
- public function createRejected(\Throwable $reason): Promise;
-
- /**
- * Given an iterable of promises (or values), returns a promise that is fulfilled when all the
- * items in the iterable are fulfilled.
- *
- * @param iterable<Promise|mixed> $promisesOrValues
- *
- * @api
- */
- public function all(iterable $promisesOrValues): Promise;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php
deleted file mode 100644
index 0416561bb14..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-
-class PromiseExecutor implements ExecutorImplementation
-{
- private Promise $result;
-
- public function __construct(Promise $result)
- {
- $this->result = $result;
- }
-
- public function doExecute(): Promise
- {
- return $this->result;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php
deleted file mode 100644
index 707434168db..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php
+++ /dev/null
@@ -1,1501 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Warning;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-import-type FieldResolver from Executor
- * @phpstan-import-type Path from ResolveInfo
- * @phpstan-import-type ArgsMapper from Executor
- *
- * @phpstan-type Fields \ArrayObject<string, \ArrayObject<int, FieldNode>>
- */
-class ReferenceExecutor implements ExecutorImplementation
-{
- protected static \stdClass $UNDEFINED;
-
- protected ExecutionContext $exeContext;
-
- /**
- * @var \SplObjectStorage<
- * ObjectType,
- * \SplObjectStorage<
- * \ArrayObject<int, FieldNode>,
- * \ArrayObject<
- * string,
- * \ArrayObject<int, FieldNode>
- * >
- * >
- * >
- */
- protected \SplObjectStorage $subFieldCache;
-
- /**
- * @var \SplObjectStorage<
- * FieldDefinition,
- * \SplObjectStorage<FieldNode, mixed>
- * >
- */
- protected \SplObjectStorage $fieldArgsCache;
-
- protected FieldDefinition $schemaMetaFieldDef;
-
- protected FieldDefinition $typeMetaFieldDef;
-
- protected FieldDefinition $typeNameMetaFieldDef;
-
- protected function __construct(ExecutionContext $context)
- {
- if (! isset(static::$UNDEFINED)) {
- static::$UNDEFINED = Utils::undefined();
- }
-
- $this->exeContext = $context;
- $this->subFieldCache = new \SplObjectStorage();
- $this->fieldArgsCache = new \SplObjectStorage();
- }
-
- /**
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed> $variableValues
- *
- * @phpstan-param FieldResolver $fieldResolver
- * @phpstan-param ArgsMapper $argsMapper
- *
- * @throws \Exception
- */
- public static function create(
- PromiseAdapter $promiseAdapter,
- Schema $schema,
- DocumentNode $documentNode,
- $rootValue,
- $contextValue,
- array $variableValues,
- ?string $operationName,
- callable $fieldResolver,
- ?callable $argsMapper = null // TODO make non-optional in next major release
- ): ExecutorImplementation {
- $exeContext = static::buildExecutionContext(
- $schema,
- $documentNode,
- $rootValue,
- $contextValue,
- $variableValues,
- $operationName,
- $fieldResolver,
- $argsMapper ?? Executor::getDefaultArgsMapper(),
- $promiseAdapter,
- );
-
- if (is_array($exeContext)) {
- $executionResult = new ExecutionResult(null, $exeContext);
- $fulfilledPromise = $promiseAdapter->createFulfilled($executionResult);
-
- return new PromiseExecutor($fulfilledPromise);
- }
-
- return new static($exeContext);
- }
-
- /**
- * Constructs an ExecutionContext object from the arguments passed to execute,
- * which we will pass throughout the other execution methods.
- *
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed> $rawVariableValues
- *
- * @phpstan-param FieldResolver $fieldResolver
- *
- * @throws \Exception
- *
- * @return ExecutionContext|list<Error>
- */
- protected static function buildExecutionContext(
- Schema $schema,
- DocumentNode $documentNode,
- $rootValue,
- $contextValue,
- array $rawVariableValues,
- ?string $operationName,
- callable $fieldResolver,
- callable $argsMapper,
- PromiseAdapter $promiseAdapter
- ) {
- /** @var list<Error> $errors */
- $errors = [];
-
- /** @var array<string, FragmentDefinitionNode> $fragments */
- $fragments = [];
-
- /** @var OperationDefinitionNode|null $operation */
- $operation = null;
-
- /** @var bool $hasMultipleAssumedOperations */
- $hasMultipleAssumedOperations = false;
-
- foreach ($documentNode->definitions as $definition) {
- switch (true) {
- case $definition instanceof OperationDefinitionNode:
- if ($operationName === null && $operation !== null) {
- $hasMultipleAssumedOperations = true;
- }
-
- if (
- $operationName === null
- || (isset($definition->name) && $definition->name->value === $operationName)
- ) {
- $operation = $definition;
- }
-
- break;
- case $definition instanceof FragmentDefinitionNode:
- $fragments[$definition->name->value] = $definition;
- break;
- }
- }
-
- if ($operation === null) {
- $message = $operationName === null
- ? 'Must provide an operation.'
- : "Unknown operation named \"{$operationName}\".";
- $errors[] = new Error($message);
- } elseif ($hasMultipleAssumedOperations) {
- $errors[] = new Error(
- 'Must provide operation name if query contains multiple operations.'
- );
- }
-
- $variableValues = null;
- if ($operation !== null) {
- [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
- $schema,
- $operation->variableDefinitions,
- $rawVariableValues
- );
- if ($coercionErrors === null) {
- $variableValues = $coercedVariableValues;
- } else {
- $errors = array_merge($errors, $coercionErrors);
- }
- }
-
- if ($errors !== []) {
- return $errors;
- }
-
- assert($operation instanceof OperationDefinitionNode, 'Has operation if no errors.');
- assert(is_array($variableValues), 'Has variables if no errors.');
-
- return new ExecutionContext(
- $schema,
- $fragments,
- $rootValue,
- $contextValue,
- $operation,
- $variableValues,
- $errors,
- $fieldResolver,
- $argsMapper,
- $promiseAdapter
- );
- }
-
- /**
- * @throws \Exception
- * @throws Error
- */
- public function doExecute(): Promise
- {
- // Return a Promise that will eventually resolve to the data described by
- // the "Response" section of the Automattic\WooCommerce\Vendor\GraphQL specification.
- //
- // If errors are encountered while executing a Automattic\WooCommerce\Vendor\GraphQL field, only that
- // field and its descendants will be omitted, and sibling fields will still
- // be executed. An execution which encounters errors will still result in a
- // resolved Promise.
- $data = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
- $result = $this->buildResponse($data);
-
- // Note: we deviate here from the reference implementation a bit by always returning promise
- // But for the "sync" case it is always fulfilled
-
- $promise = $this->getPromise($result);
- if ($promise !== null) {
- return $promise;
- }
-
- return $this->exeContext->promiseAdapter->createFulfilled($result);
- }
-
- /**
- * @param mixed $data
- *
- * @return ExecutionResult|Promise
- */
- protected function buildResponse($data)
- {
- if ($data instanceof Promise) {
- return $data->then(fn ($resolved) => $this->buildResponse($resolved));
- }
-
- $promiseAdapter = $this->exeContext->promiseAdapter;
- if ($promiseAdapter->isThenable($data)) {
- return $promiseAdapter->convertThenable($data)
- ->then(fn ($resolved) => $this->buildResponse($resolved));
- }
-
- if ($data !== null) {
- $data = (array) $data;
- }
-
- return new ExecutionResult($data, $this->exeContext->errors);
- }
-
- /**
- * Implements the "Evaluating operations" section of the spec.
- *
- * @param mixed $rootValue
- *
- * @throws \Exception
- *
- * @return array<mixed>|Promise|\stdClass|null
- */
- protected function executeOperation(OperationDefinitionNode $operation, $rootValue)
- {
- $type = $this->getOperationRootType($this->exeContext->schema, $operation);
- $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
- $path = [];
- $unaliasedPath = [];
- // Errors from sub-fields of a NonNull type may propagate to the top level,
- // at which point we still log the error and null the parent field, which
- // in this case is the entire response.
- //
- // Similar to completeValueCatchingError.
- try {
- $result = $operation->operation === 'mutation'
- ? $this->executeFieldsSerially($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue)
- : $this->executeFields($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue);
-
- $promise = $this->getPromise($result);
- if ($promise !== null) {
- return $promise->then(null, [$this, 'onError']);
- }
-
- return $result;
- } catch (Error $error) {
- $this->exeContext->addError($error);
-
- return null;
- }
- }
-
- /** @param mixed $error */
- public function onError($error): ?Promise
- {
- if ($error instanceof Error) {
- $this->exeContext->addError($error);
-
- return $this->exeContext->promiseAdapter->createFulfilled();
- }
-
- return null;
- }
-
- /**
- * Extracts the root type of the operation from the schema.
- *
- * @throws \Exception
- * @throws Error
- */
- protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation): ObjectType
- {
- switch ($operation->operation) {
- case 'query':
- $queryType = $schema->getQueryType();
- if ($queryType === null) {
- throw new Error('Schema does not define the required query root type.', [$operation]);
- }
-
- return $queryType;
-
- case 'mutation':
- $mutationType = $schema->getMutationType();
- if ($mutationType === null) {
- throw new Error('Schema is not configured for mutations.', [$operation]);
- }
-
- return $mutationType;
-
- case 'subscription':
- $subscriptionType = $schema->getSubscriptionType();
- if ($subscriptionType === null) {
- throw new Error('Schema is not configured for subscriptions.', [$operation]);
- }
-
- return $subscriptionType;
-
- default:
- throw new Error('Can only execute queries, mutations and subscriptions.', [$operation]);
- }
- }
-
- /**
- * Given a selectionSet, adds all fields in that selection to
- * the passed in map of fields, and returns it at the end.
- *
- * CollectFields requires the "runtime type" of an object. For a field which
- * returns an Interface or Union type, the "runtime type" will be the actual
- * Object type returned by that field.
- *
- * @param \ArrayObject<string, true> $visitedFragmentNames
- *
- * @phpstan-param Fields $fields
- *
- * @throws \Exception
- * @throws Error
- *
- * @phpstan-return Fields
- */
- protected function collectFields(
- ObjectType $runtimeType,
- SelectionSetNode $selectionSet,
- \ArrayObject $fields,
- \ArrayObject $visitedFragmentNames
- ): \ArrayObject {
- $exeContext = $this->exeContext;
- foreach ($selectionSet->selections as $selection) {
- switch (true) {
- case $selection instanceof FieldNode:
- if (! $this->shouldIncludeNode($selection)) {
- break;
- }
-
- $name = static::getFieldEntryKey($selection);
- $fields[$name] ??= new \ArrayObject();
- $fields[$name][] = $selection;
- break;
- case $selection instanceof InlineFragmentNode:
- if (
- ! $this->shouldIncludeNode($selection)
- || ! $this->doesFragmentConditionMatch($selection, $runtimeType)
- ) {
- break;
- }
-
- $this->collectFields(
- $runtimeType,
- $selection->selectionSet,
- $fields,
- $visitedFragmentNames
- );
- break;
- case $selection instanceof FragmentSpreadNode:
- $fragName = $selection->name->value;
-
- if (isset($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
- break;
- }
-
- $visitedFragmentNames[$fragName] = true;
-
- if (! isset($exeContext->fragments[$fragName])) {
- break;
- }
-
- $fragment = $exeContext->fragments[$fragName];
- if (! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
- break;
- }
-
- $this->collectFields(
- $runtimeType,
- $fragment->selectionSet,
- $fields,
- $visitedFragmentNames
- );
- break;
- }
- }
-
- return $fields;
- }
-
- /**
- * Determines if a field should be included based on the @include and @skip
- * directives, where @skip has higher precedence than @include.
- *
- * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
- *
- * @throws \Exception
- * @throws Error
- */
- protected function shouldIncludeNode(SelectionNode $node): bool
- {
- $variableValues = $this->exeContext->variableValues;
-
- $schema = $this->exeContext->schema;
-
- $skip = Values::getDirectiveValues(
- Directive::skipDirective(),
- $node,
- $variableValues,
- $schema,
- );
- if (isset($skip['if']) && $skip['if'] === true) {
- return false;
- }
-
- $include = Values::getDirectiveValues(
- Directive::includeDirective(),
- $node,
- $variableValues,
- $schema,
- );
-
- return ! isset($include['if']) || $include['if'] !== false;
- }
-
- /** Implements the logic to compute the key of a given fields entry. */
- protected static function getFieldEntryKey(FieldNode $node): string
- {
- return $node->alias->value
- ?? $node->name->value;
- }
-
- /**
- * Determines if a fragment is applicable to the given type.
- *
- * @param FragmentDefinitionNode|InlineFragmentNode $fragment
- *
- * @throws \Exception
- */
- protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type): bool
- {
- $typeConditionNode = $fragment->typeCondition;
- if ($typeConditionNode === null) {
- return true;
- }
-
- $conditionalType = AST::typeFromAST([$this->exeContext->schema, 'getType'], $typeConditionNode);
- if ($conditionalType === $type) {
- return true;
- }
-
- if ($conditionalType instanceof AbstractType) {
- return $this->exeContext->schema->isSubType($conditionalType, $type);
- }
-
- return false;
- }
-
- /**
- * Implements the "Evaluating selection sets" section of the spec for "write" mode.
- *
- * @param mixed $rootValue
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $contextValue
- *
- * @phpstan-param Fields $fields
- *
- * @return array<mixed>|Promise|\stdClass
- */
- protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue)
- {
- $result = $this->promiseReduce(
- array_keys($fields->getArrayCopy()),
- function ($results, $responseName) use ($contextValue, $path, $unaliasedPath, $parentType, $rootValue, $fields) {
- $fieldNodes = $fields[$responseName];
- assert($fieldNodes instanceof \ArrayObject, 'The keys of $fields populate $responseName');
-
- $result = $this->resolveField(
- $parentType,
- $rootValue,
- $fieldNodes,
- $responseName,
- $path,
- $unaliasedPath,
- $this->maybeScopeContext($contextValue)
- );
- if ($result === static::$UNDEFINED) {
- return $results;
- }
-
- $promise = $this->getPromise($result);
- if ($promise !== null) {
- return $promise->then(static function ($resolvedResult) use ($responseName, $results): array {
- $results[$responseName] = $resolvedResult;
-
- return $results;
- });
- }
-
- $results[$responseName] = $result;
-
- return $results;
- },
- []
- );
-
- $promise = $this->getPromise($result);
- if ($promise !== null) {
- return $result->then(
- static fn ($resolvedResults) => static::fixResultsIfEmptyArray($resolvedResults)
- );
- }
-
- return static::fixResultsIfEmptyArray($result);
- }
-
- /**
- * Resolves the field on the given root value.
- *
- * In particular, this figures out the value that the field returns
- * by calling its resolve function, then calls completeValue to complete promises,
- * serialize scalars, or execute the sub-selection-set for objects.
- *
- * @param mixed $rootValue
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $contextValue
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- *
- * @phpstan-param Path $path
- * @phpstan-param Path $unaliasedPath
- *
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<mixed>|\Throwable|mixed|null
- */
- protected function resolveField(
- ObjectType $parentType,
- $rootValue,
- \ArrayObject $fieldNodes,
- string $responseName,
- array $path,
- array $unaliasedPath,
- $contextValue
- ) {
- $exeContext = $this->exeContext;
-
- $fieldNode = $fieldNodes[0];
- assert($fieldNode instanceof FieldNode, '$fieldNodes is non-empty');
-
- $fieldName = $fieldNode->name->value;
- $fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
- if ($fieldDef === null || ! $fieldDef->isVisible()) {
- return static::$UNDEFINED;
- }
-
- $path[] = $responseName;
- $unaliasedPath[] = $fieldName;
-
- $returnType = $fieldDef->getType();
- // The resolve function's optional 3rd argument is a context value that
- // is provided to every resolve function within an execution. It is commonly
- // used to represent an authenticated user, or request-specific caches.
- // The resolve function's optional 4th argument is a collection of
- // information about the current execution state.
- $info = new ResolveInfo(
- $fieldDef,
- $fieldNodes,
- $parentType,
- $path,
- $exeContext->schema,
- $exeContext->fragments,
- $exeContext->rootValue,
- $exeContext->operation,
- $exeContext->variableValues,
- $unaliasedPath
- );
-
- $resolveFn = $fieldDef->resolveFn
- ?? $parentType->resolveFieldFn
- ?? $this->exeContext->fieldResolver;
-
- $argsMapper = $fieldDef->argsMapper
- ?? $parentType->argsMapper
- ?? $this->exeContext->argsMapper;
-
- // Get the resolve function, regardless of if its result is normal
- // or abrupt (error).
- $result = $this->resolveFieldValueOrError(
- $fieldDef,
- $fieldNode,
- $resolveFn,
- $argsMapper,
- $rootValue,
- $info,
- $contextValue
- );
-
- return $this->completeValueCatchingError(
- $returnType,
- $fieldNodes,
- $info,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- );
- }
-
- /**
- * This method looks up the field on the given type definition.
- *
- * It has special casing for the two introspection fields, __schema
- * and __typename. __typename is special because it can always be
- * queried as a field, even in situations where no other fields
- * are allowed, like on a Union. __schema could get automatically
- * added to the query type, but that would require mutating type
- * definitions, which would cause issues.
- *
- * @throws InvariantViolation
- */
- protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition
- {
- $this->schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef();
- $this->typeMetaFieldDef ??= Introspection::typeMetaFieldDef();
- $this->typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef();
-
- $queryType = $schema->getQueryType();
-
- if ($fieldName === $this->schemaMetaFieldDef->name
- && $queryType === $parentType
- ) {
- return $this->schemaMetaFieldDef;
- }
-
- if ($fieldName === $this->typeMetaFieldDef->name
- && $queryType === $parentType
- ) {
- return $this->typeMetaFieldDef;
- }
-
- if ($fieldName === $this->typeNameMetaFieldDef->name) {
- return $this->typeNameMetaFieldDef;
- }
-
- return $parentType->findField($fieldName);
- }
-
- /**
- * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
- * Returns the result of resolveFn or the abrupt-return Error object.
- *
- * @param mixed $rootValue
- * @param mixed $contextValue
- *
- * @phpstan-param FieldResolver $resolveFn
- *
- * @return \Throwable|Promise|mixed
- */
- protected function resolveFieldValueOrError(
- FieldDefinition $fieldDef,
- FieldNode $fieldNode,
- callable $resolveFn,
- callable $argsMapper,
- $rootValue,
- ResolveInfo $info,
- $contextValue
- ) {
- try {
- // Build a map of arguments from the field.arguments AST, using the
- // variables scope to fulfill any variable references.
- // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation
- $this->fieldArgsCache[$fieldDef] ??= new \SplObjectStorage();
-
- $args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues(
- $fieldDef,
- $fieldNode,
- $this->exeContext->variableValues,
- $this->exeContext->schema,
- ), $fieldDef, $fieldNode, $contextValue);
-
- return $resolveFn($rootValue, $args, $contextValue, $info);
- } catch (\Throwable $error) {
- return $error;
- }
- }
-
- /**
- * This is a small wrapper around completeValue which detects and logs errors
- * in the execution context.
- *
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $contextValue
- * @param mixed $result
- *
- * @phpstan-param Path $path
- * @phpstan-param Path $unaliasedPath
- *
- * @throws Error
- *
- * @return array<mixed>|Promise|\stdClass|null
- */
- protected function completeValueCatchingError(
- Type $returnType,
- \ArrayObject $fieldNodes,
- ResolveInfo $info,
- array $path,
- array $unaliasedPath,
- $result,
- $contextValue
- ) {
- // Otherwise, error protection is applied, logging the error and resolving
- // a null value for this field if one is encountered.
- try {
- $promise = $this->getPromise($result);
- if ($promise !== null) {
- $completed = $promise->then(fn (&$resolved) => $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $resolved, $contextValue));
- } else {
- $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
- }
-
- $promise = $this->getPromise($completed);
- if ($promise !== null) {
- return $promise->then(null, function ($error) use ($fieldNodes, $path, $unaliasedPath, $returnType): void {
- $this->handleFieldError($error, $fieldNodes, $path, $unaliasedPath, $returnType);
- });
- }
-
- return $completed;
- } catch (\Throwable $err) {
- $this->handleFieldError($err, $fieldNodes, $path, $unaliasedPath, $returnType);
-
- return null;
- }
- }
-
- /**
- * @param mixed $rawError
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- *
- * @throws Error
- */
- protected function handleFieldError($rawError, \ArrayObject $fieldNodes, array $path, array $unaliasedPath, Type $returnType): void
- {
- $error = Error::createLocatedError(
- $rawError,
- $fieldNodes,
- $path,
- $unaliasedPath
- );
-
- // If the field type is non-nullable, then it is resolved without any
- // protection from errors, however it still properly locates the error.
- if ($returnType instanceof NonNull) {
- throw $error;
- }
-
- // Otherwise, error protection is applied, logging the error and resolving
- // a null value for this field if one is encountered.
- $this->exeContext->addError($error);
- }
-
- /**
- * Implements the instructions for completeValue as defined in the
- * "Field entries" section of the spec.
- *
- * If the field type is Non-Null, then this recursively completes the value
- * for the inner type. It throws a field error if that completion returns null,
- * as per the "Nullability" section of the spec.
- *
- * If the field type is a List, then this recursively completes the value
- * for the inner type on each item in the list.
- *
- * If the field type is a Scalar or Enum, ensures the completed value is a legal
- * value of the type by calling the `serialize` method of Automattic\WooCommerce\Vendor\GraphQL type
- * definition.
- *
- * If the field is an abstract type, determine the runtime type of the value
- * and then complete based on that type.
- *
- * Otherwise, the field type expects a sub-selection set, and will complete the
- * value by evaluating all sub-selections.
- *
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $result
- * @param mixed $contextValue
- *
- * @throws \Throwable
- * @throws Error
- *
- * @return array<mixed>|mixed|Promise|null
- */
- protected function completeValue(
- Type $returnType,
- \ArrayObject $fieldNodes,
- ResolveInfo $info,
- array $path,
- array $unaliasedPath,
- $result,
- $contextValue
- ) {
- // If result is an Error, throw a located error.
- if ($result instanceof \Throwable) {
- throw $result;
- }
-
- // If field type is NonNull, complete for inner type, and throw field error
- // if result is null.
- if ($returnType instanceof NonNull) {
- $completed = $this->completeValue(
- $returnType->getWrappedType(),
- $fieldNodes,
- $info,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- );
- if ($completed === null) {
- throw new InvariantViolation("Cannot return null for non-nullable field \"{$info->parentType}.{$info->fieldName}\".");
- }
-
- return $completed;
- }
-
- if ($result === null) {
- return null;
- }
-
- // If field type is List, complete each item in the list with the inner type
- if ($returnType instanceof ListOfType) {
- if (! is_iterable($result)) {
- $resultType = gettype($result);
-
- throw new InvariantViolation("Expected field {$info->parentType}.{$info->fieldName} to return iterable, but got: {$resultType}.");
- }
-
- return $this->completeListValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
- }
-
- assert($returnType instanceof NamedType, 'Wrapping types should return early');
-
- // Account for invalid schema definition when typeLoader returns different
- // instance than `resolveType` or $field->getType() or $arg->getType()
- assert(
- $returnType === $this->exeContext->schema->getType($returnType->name)
- || Type::isBuiltInScalar($returnType),
- SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name)
- );
-
- if ($returnType instanceof LeafType) {
- if (Type::isBuiltInScalar($returnType)) {
- $schemaType = $this->exeContext->schema->getType($returnType->name);
- assert($schemaType instanceof LeafType, "Schema must provide a LeafType for built-in scalar \"{$returnType->name}\".");
- $returnType = $schemaType;
- }
-
- return $this->completeLeafValue($returnType, $result);
- }
-
- if ($returnType instanceof AbstractType) {
- return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
- }
-
- // Field type must be and Object, Interface or Union and expect sub-selections.
- if ($returnType instanceof ObjectType) {
- return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
- }
-
- $safeReturnType = Utils::printSafe($returnType);
- throw new \RuntimeException("Cannot complete value of unexpected type {$safeReturnType}.");
- }
-
- /** @param mixed $value */
- protected function isPromise($value): bool
- {
- return $value instanceof Promise
- || $this->exeContext->promiseAdapter->isThenable($value);
- }
-
- /**
- * Only returns the value if it acts like a Promise, i.e. has a "then" function,
- * otherwise returns null.
- *
- * @param mixed $value
- */
- protected function getPromise($value): ?Promise
- {
- if ($value === null || $value instanceof Promise) {
- return $value;
- }
-
- $promiseAdapter = $this->exeContext->promiseAdapter;
- if ($promiseAdapter->isThenable($value)) {
- return $promiseAdapter->convertThenable($value);
- }
-
- return null;
- }
-
- /**
- * Similar to array_reduce(), however the reducing callback may return
- * a Promise, in which case reduction will continue after each promise resolves.
- *
- * If the callback does not return a Promise, then this function will also not
- * return a Promise.
- *
- * @param array<mixed> $values
- * @param Promise|mixed|null $initialValue
- *
- * @return Promise|mixed|null
- */
- protected function promiseReduce(array $values, callable $callback, $initialValue)
- {
- return array_reduce(
- $values,
- function ($previous, $value) use ($callback) {
- $promise = $this->getPromise($previous);
- if ($promise !== null) {
- return $promise->then(static fn ($resolved) => $callback($resolved, $value));
- }
-
- return $callback($previous, $value);
- },
- $initialValue
- );
- }
-
- /**
- * Complete a list value by completing each item in the list with the inner type.
- *
- * @param ListOfType<Type&OutputType> $returnType
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param iterable<mixed> $results
- * @param mixed $contextValue
- *
- * @throws Error
- *
- * @return array<mixed>|Promise|\stdClass
- */
- protected function completeListValue(
- ListOfType $returnType,
- \ArrayObject $fieldNodes,
- ResolveInfo $info,
- array $path,
- array $unaliasedPath,
- iterable $results,
- $contextValue
- ) {
- $itemType = $returnType->getWrappedType();
-
- $i = 0;
- $containsPromise = false;
- $completedItems = [];
- foreach ($results as $item) {
- $itemPath = [...$path, $i];
- $info->path = $itemPath;
- $itemUnaliasedPath = [...$unaliasedPath, $i];
- $info->unaliasedPath = $itemUnaliasedPath;
- ++$i;
-
- $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $itemPath, $itemUnaliasedPath, $item, $contextValue);
-
- if (! $containsPromise && $this->getPromise($completedItem) !== null) {
- $containsPromise = true;
- }
-
- $completedItems[] = $completedItem;
- }
-
- return $containsPromise
- ? $this->exeContext->promiseAdapter->all($completedItems)
- : $completedItems;
- }
-
- /**
- * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
- *
- * @param mixed $result
- *
- * @throws \Exception
- *
- * @return mixed
- */
- protected function completeLeafValue(LeafType $returnType, $result)
- {
- try {
- return $returnType->serialize($result);
- } catch (\Throwable $error) {
- $safeReturnType = Utils::printSafe($returnType);
- $safeResult = Utils::printSafe($result);
- throw new InvariantViolation("Expected a value of type {$safeReturnType} but received: {$safeResult}. {$error->getMessage()}", 0, $error);
- }
- }
-
- /**
- * Complete a value of an abstract type by determining the runtime object type
- * of that value, then complete the value for that type.
- *
- * @param AbstractType&Type $returnType
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $result
- * @param mixed $contextValue
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<mixed>|Promise|\stdClass
- */
- protected function completeAbstractValue(
- AbstractType $returnType,
- \ArrayObject $fieldNodes,
- ResolveInfo $info,
- array $path,
- array $unaliasedPath,
- $result,
- $contextValue
- ) {
- $result = $returnType->resolveValue($result, $contextValue, $info);
- $typeCandidate = $returnType->resolveType($result, $contextValue, $info);
-
- if ($typeCandidate === null) {
- $runtimeType = static::defaultTypeResolver($result, $contextValue, $info, $returnType);
- } elseif (! is_string($typeCandidate) && is_callable($typeCandidate)) {
- $runtimeType = $typeCandidate();
- } else {
- $runtimeType = $typeCandidate;
- }
-
- $promise = $this->getPromise($runtimeType);
- if ($promise !== null) {
- return $promise->then(fn ($resolvedRuntimeType) => $this->completeObjectValue(
- $this->ensureValidRuntimeType(
- $resolvedRuntimeType,
- $returnType,
- $info,
- $result
- ),
- $fieldNodes,
- $info,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- ));
- }
-
- return $this->completeObjectValue(
- $this->ensureValidRuntimeType(
- $runtimeType,
- $returnType,
- $info,
- $result
- ),
- $fieldNodes,
- $info,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- );
- }
-
- /**
- * If a resolveType function is not given, then a default resolve behavior is
- * used which attempts two strategies:.
- *
- * First, See if the provided value has a `__typename` field defined, if so, use
- * that value as name of the resolved type.
- *
- * Otherwise, test each possible type for the abstract type by calling
- * isTypeOf for the object being coerced, returning the first type that matches.
- *
- * @param mixed|null $value
- * @param mixed|null $contextValue
- * @param AbstractType&Type $abstractType
- *
- * @throws InvariantViolation
- *
- * @return Promise|Type|string|null
- */
- protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
- {
- $typename = Utils::extractKey($value, '__typename');
- if (is_string($typename)) {
- return $typename;
- }
-
- if ($abstractType instanceof InterfaceType && isset($info->schema->getConfig()->typeLoader)) {
- $safeValue = Utils::printSafe($value);
- Warning::warnOnce(
- "Automattic\WooCommerce\Vendor\GraphQL Interface Type `{$abstractType->name}` returned `null` from its `resolveType` function for value: {$safeValue}. Switching to slow resolution method using `isTypeOf` of all possible implementations. It requires full schema scan and degrades query performance significantly. Make sure your `resolveType` function always returns a valid implementation or throws.",
- Warning::WARNING_FULL_SCHEMA_SCAN
- );
- }
-
- $possibleTypes = $info->schema->getPossibleTypes($abstractType);
- $promisedIsTypeOfResults = [];
- foreach ($possibleTypes as $index => $type) {
- $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info);
- if ($isTypeOfResult === null) {
- continue;
- }
-
- $promise = $this->getPromise($isTypeOfResult);
- if ($promise !== null) {
- $promisedIsTypeOfResults[$index] = $promise;
- } elseif ($isTypeOfResult === true) {
- return $type;
- }
- }
-
- if ($promisedIsTypeOfResults !== []) {
- return $this->exeContext->promiseAdapter
- ->all($promisedIsTypeOfResults)
- ->then(static function ($isTypeOfResults) use ($possibleTypes): ?ObjectType {
- foreach ($isTypeOfResults as $index => $result) {
- if ($result) {
- return $possibleTypes[$index];
- }
- }
-
- return null;
- });
- }
-
- return null;
- }
-
- /**
- * Complete an Object value by executing all sub-selections.
- *
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $result
- * @param mixed $contextValue
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<mixed>|Promise|\stdClass
- */
- protected function completeObjectValue(
- ObjectType $returnType,
- \ArrayObject $fieldNodes,
- ResolveInfo $info,
- array $path,
- array $unaliasedPath,
- $result,
- $contextValue
- ) {
- // If there is an isTypeOf predicate function, call it with the
- // current result. If isTypeOf returns false, then raise an error rather
- // than continuing execution.
- $isTypeOf = $returnType->isTypeOf($result, $contextValue, $info);
- if ($isTypeOf !== null) {
- $promise = $this->getPromise($isTypeOf);
- if ($promise !== null) {
- return $promise->then(function ($isTypeOfResult) use (
- $contextValue,
- $returnType,
- $fieldNodes,
- $path,
- $unaliasedPath,
- $result
- ) {
- if (! $isTypeOfResult) {
- throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
- }
-
- return $this->collectAndExecuteSubfields(
- $returnType,
- $fieldNodes,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- );
- });
- }
-
- assert(is_bool($isTypeOf), 'Promise would return early');
- if (! $isTypeOf) {
- throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
- }
- }
-
- return $this->collectAndExecuteSubfields(
- $returnType,
- $fieldNodes,
- $path,
- $unaliasedPath,
- $result,
- $contextValue
- );
- }
-
- /**
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param mixed $result
- */
- protected function invalidReturnTypeError(
- ObjectType $returnType,
- $result,
- \ArrayObject $fieldNodes
- ): Error {
- $safeResult = Utils::printSafe($result);
-
- return new Error(
- "Expected value of type \"{$returnType->name}\" but got: {$safeResult}.",
- $fieldNodes
- );
- }
-
- /**
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $result
- * @param mixed $contextValue
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<mixed>|Promise|\stdClass
- */
- protected function collectAndExecuteSubfields(
- ObjectType $returnType,
- \ArrayObject $fieldNodes,
- array $path,
- array $unaliasedPath,
- $result,
- $contextValue
- ) {
- $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
-
- return $this->executeFields($returnType, $result, $path, $unaliasedPath, $subFieldNodes, $contextValue);
- }
-
- /**
- * A memoized collection of relevant subfields with regard to the return
- * type. Memoizing ensures the subfields are not repeatedly calculated, which
- * saves overhead when resolving lists of values.
- *
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- *
- * @throws \Exception
- * @throws Error
- *
- * @phpstan-return Fields
- */
- protected function collectSubFields(ObjectType $returnType, \ArrayObject $fieldNodes): \ArrayObject
- {
- // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation
- $returnTypeCache = $this->subFieldCache[$returnType] ??= new \SplObjectStorage();
-
- if (! isset($returnTypeCache[$fieldNodes])) {
- // Collect sub-fields to execute to complete this value.
- $subFieldNodes = new \ArrayObject();
- $visitedFragmentNames = new \ArrayObject();
- foreach ($fieldNodes as $fieldNode) {
- if (isset($fieldNode->selectionSet)) {
- $subFieldNodes = $this->collectFields(
- $returnType,
- $fieldNode->selectionSet,
- $subFieldNodes,
- $visitedFragmentNames
- );
- }
- }
-
- $returnTypeCache[$fieldNodes] = $subFieldNodes;
- }
-
- return $returnTypeCache[$fieldNodes];
- }
-
- /**
- * Implements the "Evaluating selection sets" section of the spec for "read" mode.
- *
- * @param mixed $rootValue
- * @param list<string|int> $path
- * @param list<string|int> $unaliasedPath
- * @param mixed $contextValue
- *
- * @phpstan-param Fields $fields
- *
- * @throws Error
- * @throws InvariantViolation
- *
- * @return Promise|\stdClass|array<mixed>
- */
- protected function executeFields(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue)
- {
- $containsPromise = false;
- $results = [];
- foreach ($fields as $responseName => $fieldNodes) {
- $result = $this->resolveField(
- $parentType,
- $rootValue,
- $fieldNodes,
- $responseName,
- $path,
- $unaliasedPath,
- $this->maybeScopeContext($contextValue)
- );
- if ($result === static::$UNDEFINED) {
- continue;
- }
-
- if (! $containsPromise && $this->isPromise($result)) {
- $containsPromise = true;
- }
-
- $results[$responseName] = $result;
- }
-
- // If there are no promises, we can just return the object
- if (! $containsPromise) {
- return static::fixResultsIfEmptyArray($results);
- }
-
- // Otherwise, results is a map from field name to the result of resolving that
- // field, which is possibly a promise. Return a promise that will return this
- // same map, but with any promises replaced with the values they resolved to.
- return $this->promiseForAssocArray($results);
- }
-
- /**
- * Differentiate empty objects from empty lists.
- *
- * @see https://github.com/webonyx/graphql-php/issues/59
- *
- * @param array<mixed>|mixed $results
- *
- * @return non-empty-array<mixed>|\stdClass|mixed
- */
- protected static function fixResultsIfEmptyArray($results)
- {
- if ($results === []) {
- return new \stdClass();
- }
-
- return $results;
- }
-
- /**
- * Transform an associative array with Promises to a Promise which resolves to an
- * associative array where all Promises were resolved.
- *
- * @param array<string, Promise|mixed> $assoc
- */
- protected function promiseForAssocArray(array $assoc): Promise
- {
- $keys = array_keys($assoc);
- $valuesAndPromises = array_values($assoc);
- $promise = $this->exeContext->promiseAdapter->all($valuesAndPromises);
-
- return $promise->then(static function ($values) use ($keys) {
- $resolvedResults = [];
- foreach ($values as $i => $value) {
- $resolvedResults[$keys[$i]] = $value;
- }
-
- return static::fixResultsIfEmptyArray($resolvedResults);
- });
- }
-
- /**
- * @param mixed $runtimeTypeOrName
- * @param AbstractType&Type $returnType
- * @param mixed $result
- *
- * @throws InvariantViolation
- */
- protected function ensureValidRuntimeType(
- $runtimeTypeOrName,
- AbstractType $returnType,
- ResolveInfo $info,
- $result
- ): ObjectType {
- $runtimeType = is_string($runtimeTypeOrName)
- ? $this->exeContext->schema->getType($runtimeTypeOrName)
- : $runtimeTypeOrName;
-
- if (! $runtimeType instanceof ObjectType) {
- $safeResult = Utils::printSafe($result);
- $notObjectType = Utils::printSafe($runtimeType);
- throw new InvariantViolation("Abstract type {$returnType} must resolve to an Object type at runtime for field {$info->parentType}.{$info->fieldName} with value {$safeResult}, received \"{$notObjectType}\". Either the {$returnType} type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.");
- }
-
- if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) {
- throw new InvariantViolation("Runtime Object type \"{$runtimeType}\" is not a possible type for \"{$returnType}\".");
- }
-
- assert(
- $this->exeContext->schema->getType($runtimeType->name) !== null,
- "Schema does not contain type \"{$runtimeType}\". This can happen when an object type is only referenced indirectly through abstract types and never directly through fields.List the type in the option \"types\" during schema construction, see https://webonyx.github.io/graphql-php/schema-definition/#configuration-options."
- );
-
- assert(
- $runtimeType === $this->exeContext->schema->getType($runtimeType->name),
- "Schema must contain unique named types but contains multiple types named \"{$runtimeType}\". Make sure that `resolveType` function of abstract type \"{$returnType}\" returns the same type instance as referenced anywhere else within the schema (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry)."
- );
-
- return $runtimeType;
- }
-
- /**
- * @param mixed $contextValue
- *
- * @return mixed
- */
- private function maybeScopeContext($contextValue)
- {
- if ($contextValue instanceof ScopedContext) {
- return $contextValue->clone();
- }
-
- return $contextValue;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php
deleted file mode 100644
index 8fcb29ed083..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-/**
- * When the object passed as `$contextValue` to Automattic\WooCommerce\Vendor\GraphQL execution implements this,
- * its `clone()` method will be called before passing the context down to a field.
- * This allows passing information to child fields in the query tree without affecting sibling or parent fields.
- */
-interface ScopedContext
-{
- public function clone(): self;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php
deleted file mode 100644
index f99a742a8db..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php
+++ /dev/null
@@ -1,279 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Value;
-
-/**
- * @see ArgumentNode - force IDE import
- *
- * @phpstan-import-type ArgumentNodeValue from ArgumentNode
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ValuesTest
- */
-class Values
-{
- /**
- * Prepares an object map of variables of the correct type based on the provided
- * variable definitions and arbitrary input. If the input cannot be coerced
- * to match the variable definitions, an Error will be thrown.
- *
- * @param NodeList<VariableDefinitionNode> $varDefNodes
- * @param array<string, mixed> $rawVariableValues
- *
- * @throws \Exception
- *
- * @return array{array<int, Error>, null}|array{null, array<string, mixed>}
- */
- public static function getVariableValues(Schema $schema, NodeList $varDefNodes, array $rawVariableValues): array
- {
- $errors = [];
- $coercedValues = [];
- foreach ($varDefNodes as $varDefNode) {
- $varName = $varDefNode->variable->name->value;
- $varType = AST::typeFromAST([$schema, 'getType'], $varDefNode->type);
-
- if (! Type::isInputType($varType)) {
- // Must use input types for variables. This should be caught during
- // validation, however is checked again here for safety.
- $typeStr = Printer::doPrint($varDefNode->type);
- $errors[] = new Error(
- "Variable \"\${$varName}\" expected value of type \"{$typeStr}\" which cannot be used as an input type.",
- [$varDefNode->type]
- );
- } else {
- $hasValue = array_key_exists($varName, $rawVariableValues);
- $value = $hasValue
- ? $rawVariableValues[$varName]
- : Utils::undefined();
-
- if (! $hasValue && ($varDefNode->defaultValue !== null)) {
- // If no value was provided to a variable with a default value,
- // use the default value.
- $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
- } elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) {
- // If no value or a nullish value was provided to a variable with a
- // non-null type (required), produce an error.
- $safeVarType = Utils::printSafe($varType);
- $message = $hasValue
- ? "Variable \"\${$varName}\" of non-null type \"{$safeVarType}\" must not be null."
- : "Variable \"\${$varName}\" of required type \"{$safeVarType}\" was not provided.";
- $errors[] = new Error($message, [$varDefNode]);
- } elseif ($hasValue) {
- if ($value === null) {
- // If the explicit value `null` was provided, an entry in the coerced
- // values must exist as the value `null`.
- $coercedValues[$varName] = null;
- } else {
- // Otherwise, a non-null value was provided, coerce it to the expected
- // type or report an error if coercion fails.
- $coerced = Value::coerceInputValue($value, $varType, null, $schema);
-
- $coercionErrors = $coerced['errors'];
- if ($coercionErrors !== null) {
- foreach ($coercionErrors as $coercionError) {
- $invalidValue = $coercionError->printInvalidValue();
-
- $inputPath = $coercionError->printInputPath();
- $pathMessage = $inputPath !== null
- ? " at \"{$varName}{$inputPath}\""
- : '';
-
- $errors[] = new Error(
- "Variable \"\${$varName}\" got invalid value {$invalidValue}{$pathMessage}; {$coercionError->getMessage()}",
- $varDefNode,
- $coercionError->getSource(),
- $coercionError->getPositions(),
- $coercionError->getPath(),
- $coercionError,
- $coercionError->getExtensions()
- );
- }
- } else {
- $coercedValues[$varName] = $coerced['value'];
- }
- }
- }
- }
- }
-
- return $errors === []
- ? [null, $coercedValues]
- : [$errors, null];
- }
-
- /**
- * Prepares an object map of argument values given a directive definition
- * and an AST node which may contain directives. Optionally also accepts a map
- * of variable values.
- *
- * If the directive does not exist on the node, returns undefined.
- *
- * @param EnumTypeDefinitionNode|EnumTypeExtensionNode|EnumValueDefinitionNode|FieldDefinitionNode|FieldNode|FragmentDefinitionNode|FragmentSpreadNode|InlineFragmentNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|OperationDefinitionNode|ScalarTypeDefinitionNode|ScalarTypeExtensionNode|SchemaExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|VariableDefinitionNode $node
- * @param array<string, mixed>|null $variableValues
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<string, mixed>|null
- */
- public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null, ?Schema $schema = null): ?array
- {
- $directiveDefName = $directiveDef->name;
-
- foreach ($node->directives as $directive) {
- if ($directive->name->value === $directiveDefName) {
- return self::getArgumentValues($directiveDef, $directive, $variableValues, $schema);
- }
- }
-
- return null;
- }
-
- /**
- * Prepares an object map of argument values given a list of argument
- * definitions and list of argument AST nodes.
- *
- * @param FieldDefinition|Directive $def
- * @param FieldNode|DirectiveNode $node
- * @param array<string, mixed>|null $variableValues
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<string, mixed>
- */
- public static function getArgumentValues($def, Node $node, ?array $variableValues = null, ?Schema $schema = null): array
- {
- if ($def->args === []) {
- return [];
- }
-
- /** @var array<string, ArgumentNodeValue> $argumentValueMap */
- $argumentValueMap = [];
-
- // Might not be defined when an AST from JS is used
- if (isset($node->arguments)) {
- foreach ($node->arguments as $argumentNode) {
- $argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
- }
- }
-
- return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node, $schema);
- }
-
- /**
- * @param FieldDefinition|Directive $def
- * @param array<string, ArgumentNodeValue> $argumentValueMap
- * @param array<string, mixed>|null $variableValues
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<string, mixed>
- */
- public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null, ?Schema $schema = null): array
- {
- /** @var array<string, mixed> $coercedValues */
- $coercedValues = [];
-
- foreach ($def->args as $argumentDefinition) {
- $name = $argumentDefinition->name;
- $argType = $argumentDefinition->getType();
- $argumentValueNode = $argumentValueMap[$name] ?? null;
-
- if ($argumentValueNode instanceof VariableNode) {
- $variableName = $argumentValueNode->name->value;
- $hasValue = $variableValues !== null && array_key_exists($variableName, $variableValues);
- $isNull = $hasValue && $variableValues[$variableName] === null;
- } else {
- $hasValue = $argumentValueNode !== null;
- $isNull = $argumentValueNode instanceof NullValueNode;
- }
-
- if (! $hasValue && $argumentDefinition->defaultValueExists()) {
- // If no argument was provided where the definition has a default value,
- // use the default value.
- $coercedValues[$name] = $argumentDefinition->defaultValue;
- } elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) {
- // If no argument or a null value was provided to an argument with a
- // non-null type (required), produce a field error.
- $safeArgType = Utils::printSafe($argType);
-
- if ($isNull) {
- throw new Error("Argument \"{$name}\" of non-null type \"{$safeArgType}\" must not be null.", $referenceNode);
- }
-
- if ($argumentValueNode instanceof VariableNode) {
- throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was provided the variable \"\${$argumentValueNode->name->value}\" which was not provided a runtime value.", [$argumentValueNode]);
- }
-
- throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was not provided.", $referenceNode);
- } elseif ($hasValue) {
- assert($argumentValueNode instanceof Node);
-
- if ($argumentValueNode instanceof NullValueNode) {
- // If the explicit value `null` was provided, an entry in the coerced
- // values must exist as the value `null`.
- $coercedValues[$name] = null;
- } elseif ($argumentValueNode instanceof VariableNode) {
- $variableName = $argumentValueNode->name->value;
- // Note: This does no further checking that this variable is correct.
- // This assumes that this query has been validated and the variable
- // usage here is of the correct type.
- $coercedValues[$name] = $variableValues[$variableName] ?? null;
- } else {
- $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues, $schema);
- if (Utils::undefined() === $coercedValue) {
- // Note: ValuesOfCorrectType validation should catch this before
- // execution. This is a runtime check to ensure execution does not
- // continue with an invalid argument value.
- $invalidValue = Printer::doPrint($argumentValueNode);
- throw new Error("Argument \"{$name}\" has invalid value {$invalidValue}.", [$argumentValueNode]);
- }
-
- $coercedValues[$name] = $coercedValue;
- }
- }
- }
-
- return $coercedValues;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php b/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php
deleted file mode 100644
index b467d5f05a8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php
+++ /dev/null
@@ -1,265 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema as SchemaType;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
-
-/**
- * This is the primary facade for fulfilling Automattic\WooCommerce\Vendor\GraphQL operations.
- * See [related documentation](executing-queries.md).
- *
- * @phpstan-import-type ArgsMapper from Executor
- * @phpstan-import-type FieldResolver from Executor
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\GraphQLTest
- */
-class GraphQL
-{
- /**
- * Executes graphql query.
- *
- * More sophisticated Automattic\WooCommerce\Vendor\GraphQL servers, such as those which persist queries,
- * may wish to separate the validation and execution phases to a static time
- * tooling step, and a server runtime step.
- *
- * Available options:
- *
- * schema:
- * The Automattic\WooCommerce\Vendor\GraphQL type system to use when validating and executing a query.
- * source:
- * A Automattic\WooCommerce\Vendor\GraphQL language formatted string representing the requested operation.
- * rootValue:
- * The value provided as the first argument to resolver functions on the top
- * level type (e.g. the query object type).
- * contextValue:
- * The context value is provided as an argument to resolver functions after
- * field arguments. It is used to pass shared information useful at any point
- * during executing this query, for example the currently logged in user and
- * connections to databases or other services.
- * If the passed object implements the `ScopedContext` interface,
- * its `clone()` method will be called before passing the context down to a field.
- * This allows passing information to child fields in the query tree without affecting sibling or parent fields.
- * variableValues:
- * A mapping of variable name to runtime value to use for all variables
- * defined in the requestString.
- * operationName:
- * The name of the operation to use if requestString contains multiple
- * possible operations. Can be omitted if requestString contains only
- * one operation.
- * fieldResolver:
- * A resolver function to use when one is not provided by the schema.
- * If not provided, the default field resolver is used (which looks for a
- * value on the source value with the field's name).
- * validationRules:
- * A set of rules for query validation step. Default value is all available rules.
- * Empty array would allow to skip query validation (may be convenient for persisted
- * queries which are validated before persisting and assumed valid during execution)
- *
- * @param string|DocumentNode $source
- * @param mixed $rootValue
- * @param mixed $contextValue
- * @param array<string, mixed>|null $variableValues
- * @param array<ValidationRule>|null $validationRules
- *
- * @api
- *
- * @throws \Exception
- * @throws InvariantViolation
- */
- public static function executeQuery(
- SchemaType $schema,
- $source,
- $rootValue = null,
- $contextValue = null,
- ?array $variableValues = null,
- ?string $operationName = null,
- ?callable $fieldResolver = null,
- ?array $validationRules = null
- ): ExecutionResult {
- $promiseAdapter = new SyncPromiseAdapter();
-
- $promise = self::promiseToExecute(
- $promiseAdapter,
- $schema,
- $source,
- $rootValue,
- $contextValue,
- $variableValues,
- $operationName,
- $fieldResolver,
- $validationRules
- );
-
- return $promiseAdapter->wait($promise);
- }
-
- /**
- * Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
- * Useful for Async PHP platforms.
- *
- * @param string|DocumentNode $source
- * @param mixed $rootValue
- * @param mixed $context
- * @param array<string, mixed>|null $variableValues
- * @param array<ValidationRule>|null $validationRules Defaults to using all available rules
- *
- * @api
- *
- * @throws \Exception
- */
- public static function promiseToExecute(
- PromiseAdapter $promiseAdapter,
- SchemaType $schema,
- $source,
- $rootValue = null,
- $context = null,
- ?array $variableValues = null,
- ?string $operationName = null,
- ?callable $fieldResolver = null,
- ?array $validationRules = null
- ): Promise {
- try {
- $documentNode = $source instanceof DocumentNode
- ? $source
- : Parser::parse(new Source($source, 'GraphQL'));
-
- if ($validationRules === null) {
- $queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
- assert($queryComplexity instanceof QueryComplexity, 'should not register a different rule for QueryComplexity');
-
- $queryComplexity->setRawVariableValues($variableValues);
- } else {
- foreach ($validationRules as $rule) {
- if ($rule instanceof QueryComplexity) {
- $rule->setRawVariableValues($variableValues);
- }
- }
- }
-
- $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
-
- if ($validationErrors !== []) {
- return $promiseAdapter->createFulfilled(
- new ExecutionResult(null, $validationErrors)
- );
- }
-
- return Executor::promiseToExecute(
- $promiseAdapter,
- $schema,
- $documentNode,
- $rootValue,
- $context,
- $variableValues,
- $operationName,
- $fieldResolver
- );
- } catch (Error $e) {
- return $promiseAdapter->createFulfilled(
- new ExecutionResult(null, [$e])
- );
- }
- }
-
- /**
- * Returns directives defined in Automattic\WooCommerce\Vendor\GraphQL spec.
- *
- * @deprecated use {@see Directive::builtInDirectives()}
- *
- * @throws InvariantViolation
- *
- * @return array<string, Directive>
- *
- * @api
- */
- public static function getStandardDirectives(): array
- {
- return Directive::builtInDirectives();
- }
-
- /**
- * Returns built-in scalar types defined in Automattic\WooCommerce\Vendor\GraphQL spec.
- *
- * @deprecated use {@see Type::builtInScalars()}
- *
- * @throws InvariantViolation
- *
- * @return array<string, ScalarType>
- *
- * @api
- */
- public static function getStandardTypes(): array
- {
- return Type::builtInScalars();
- }
-
- /**
- * Replaces standard types with types from this list (matching by name).
- *
- * Standard types not listed here remain untouched.
- *
- * @deprecated prefer per-schema scalar overrides via {@see \Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::$types} or {@see \Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::$typeLoader}
- *
- * @param array<string, ScalarType> $types
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public static function overrideStandardTypes(array $types): void
- {
- Type::overrideStandardTypes($types);
- }
-
- /**
- * Returns standard validation rules implementing Automattic\WooCommerce\Vendor\GraphQL spec.
- *
- * @return array<class-string<ValidationRule>, ValidationRule>
- *
- * @api
- */
- public static function getStandardValidationRules(): array
- {
- return DocumentValidator::defaultRules();
- }
-
- /**
- * Set default resolver implementation.
- *
- * @phpstan-param FieldResolver $fn
- *
- * @api
- */
- public static function setDefaultFieldResolver(callable $fn): void
- {
- Executor::setDefaultFieldResolver($fn);
- }
-
- /**
- * Set default args mapper implementation.
- *
- * @phpstan-param ArgsMapper $fn
- *
- * @api
- */
- public static function setDefaultArgsMapper(callable $fn): void
- {
- Executor::setDefaultArgsMapper($fn);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php
deleted file mode 100644
index 25e3625aff7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * @phpstan-type ArgumentNodeValue VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode
- */
-class ArgumentNode extends Node
-{
- public string $kind = NodeKind::ARGUMENT;
-
- /** @phpstan-var ArgumentNodeValue */
- public ValueNode $value;
-
- public NameNode $name;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php
deleted file mode 100644
index c3b8f3d8785..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class BooleanValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::BOOLEAN;
-
- public bool $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php
deleted file mode 100644
index f4baccf9f05..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type DefinitionNode =
- * | ExecutableDefinitionNode
- * | TypeSystemDefinitionNode
- * | TypeSystemExtensionNode;.
- */
-interface DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php
deleted file mode 100644
index 7e86d513e90..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
-{
- public string $kind = NodeKind::DIRECTIVE_DEFINITION;
-
- public NameNode $name;
-
- public ?StringValueNode $description = null;
-
- /** @var NodeList<InputValueDefinitionNode> */
- public NodeList $arguments;
-
- public bool $repeatable;
-
- /** @var NodeList<NameNode> */
- public NodeList $locations;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->arguments ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php
deleted file mode 100644
index d487d8e6ed2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class DirectiveNode extends Node
-{
- public string $kind = NodeKind::DIRECTIVE;
-
- public NameNode $name;
-
- /** @var NodeList<ArgumentNode> */
- public NodeList $arguments;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->arguments ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php
deleted file mode 100644
index f71632ef5d5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class DocumentNode extends Node
-{
- public string $kind = NodeKind::DOCUMENT;
-
- /** @var NodeList<DefinitionNode&Node> */
- public NodeList $definitions;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php
deleted file mode 100644
index ed520d6b1fb..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::ENUM_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<EnumValueDefinitionNode> */
- public NodeList $values;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php
deleted file mode 100644
index 1795e77588b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class EnumTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::ENUM_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<EnumValueDefinitionNode> */
- public NodeList $values;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php
deleted file mode 100644
index 474b8519208..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class EnumValueDefinitionNode extends Node
-{
- public string $kind = NodeKind::ENUM_VALUE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public ?StringValueNode $description = null;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php
deleted file mode 100644
index d30003b7bca..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class EnumValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::ENUM;
-
- public string $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php
deleted file mode 100644
index 974e5589d2b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type ExecutableDefinitionNode =
- * | OperationDefinitionNode
- * | FragmentDefinitionNode;.
- */
-interface ExecutableDefinitionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php
deleted file mode 100644
index b88577175f7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class FieldDefinitionNode extends Node
-{
- public string $kind = NodeKind::FIELD_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<InputValueDefinitionNode> */
- public NodeList $arguments;
-
- /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
- public TypeNode $type;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public ?StringValueNode $description = null;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php
deleted file mode 100644
index 03ce7dec148..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class FieldNode extends Node implements SelectionNode
-{
- public string $kind = NodeKind::FIELD;
-
- public NameNode $name;
-
- public ?NameNode $alias = null;
-
- /** @var NodeList<ArgumentNode> */
- public NodeList $arguments;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public ?SelectionSetNode $selectionSet = null;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- $this->arguments ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php
deleted file mode 100644
index 392956675df..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class FloatValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::FLOAT;
-
- public string $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php
deleted file mode 100644
index 08bc15c79e8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
-{
- public string $kind = NodeKind::FRAGMENT_DEFINITION;
-
- public NameNode $name;
-
- /**
- * Note: fragment variable definitions are experimental and may be changed
- * or removed in the future.
- *
- * Thus, this property is the single exception where this is not always a NodeList but may be null.
- *
- * @var NodeList<VariableDefinitionNode>|null
- */
- public ?NodeList $variableDefinitions = null;
-
- public NamedTypeNode $typeCondition;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public SelectionSetNode $selectionSet;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-
- public function getSelectionSet(): SelectionSetNode
- {
- return $this->selectionSet;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php
deleted file mode 100644
index e535c582eff..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class FragmentSpreadNode extends Node implements SelectionNode
-{
- public string $kind = NodeKind::FRAGMENT_SPREAD;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php
deleted file mode 100644
index 217b24fdfea..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type DefinitionNode = OperationDefinitionNode
- * | FragmentDefinitionNode.
- */
-interface HasSelectionSet
-{
- public function getSelectionSet(): SelectionSetNode;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php
deleted file mode 100644
index 33da8fcf68c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InlineFragmentNode extends Node implements SelectionNode
-{
- public string $kind = NodeKind::INLINE_FRAGMENT;
-
- public ?NamedTypeNode $typeCondition = null;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public SelectionSetNode $selectionSet;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php
deleted file mode 100644
index ff503b486b4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<InputValueDefinitionNode> */
- public NodeList $fields;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php
deleted file mode 100644
index b8102c24f68..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<InputValueDefinitionNode> */
- public NodeList $fields;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php
deleted file mode 100644
index 8194f7cac42..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InputValueDefinitionNode extends Node
-{
- public string $kind = NodeKind::INPUT_VALUE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
- public TypeNode $type;
-
- /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
- public ?ValueNode $defaultValue = null;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public ?StringValueNode $description = null;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php
deleted file mode 100644
index c4def4465c1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class IntValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::INT;
-
- public string $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php
deleted file mode 100644
index ab54f1073d4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $interfaces;
-
- /** @var NodeList<FieldDefinitionNode> */
- public NodeList $fields;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php
deleted file mode 100644
index c1342bad446..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $interfaces;
-
- /** @var NodeList<FieldDefinitionNode> */
- public NodeList $fields;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php
deleted file mode 100644
index 63d58931763..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ListTypeNode extends Node implements TypeNode
-{
- public string $kind = NodeKind::LIST_TYPE;
-
- /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
- public TypeNode $type;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php
deleted file mode 100644
index f52a71bf5d8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ListValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::LST;
-
- /** @var NodeList<ValueNode&Node> */
- public NodeList $values;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php
deleted file mode 100644
index 38ecbba0999..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Token;
-
-/**
- * Contains a range of UTF-8 character offsets and token references that
- * identify the region of the source from which the AST derived.
- *
- * @phpstan-type LocationArray array{start: int, end: int}
- */
-class Location
-{
- /** The character offset at which this Node begins. */
- public int $start;
-
- /** The character offset at which this Node ends. */
- public int $end;
-
- /** The Token at which this Node begins. */
- public ?Token $startToken = null;
-
- /** The Token at which this Node ends. */
- public ?Token $endToken = null;
-
- /** The Source document the AST represents. */
- public ?Source $source = null;
-
- public static function create(int $start, int $end): self
- {
- $tmp = new static();
-
- $tmp->start = $start;
- $tmp->end = $end;
-
- return $tmp;
- }
-
- public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null)
- {
- $this->startToken = $startToken;
- $this->endToken = $endToken;
- $this->source = $source;
-
- if ($startToken === null || $endToken === null) {
- return;
- }
-
- $this->start = $startToken->start;
- $this->end = $endToken->end;
- }
-
- /** @return LocationArray */
- public function toArray(): array
- {
- return [
- 'start' => $this->start,
- 'end' => $this->end,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php
deleted file mode 100644
index 77e0605a0ae..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class NameNode extends Node implements TypeNode
-{
- public string $kind = NodeKind::NAME;
-
- public string $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php
deleted file mode 100644
index ce51a50d18b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class NamedTypeNode extends Node implements TypeNode
-{
- public string $kind = NodeKind::NAMED_TYPE;
-
- public NameNode $name;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php
deleted file mode 100644
index bd474c59038..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * type Node = NameNode
- * | DocumentNode
- * | OperationDefinitionNode
- * | VariableDefinitionNode
- * | VariableNode
- * | SelectionSetNode
- * | FieldNode
- * | ArgumentNode
- * | FragmentSpreadNode
- * | InlineFragmentNode
- * | FragmentDefinitionNode
- * | IntValueNode
- * | FloatValueNode
- * | StringValueNode
- * | BooleanValueNode
- * | EnumValueNode
- * | ListValueNode
- * | ObjectValueNode
- * | ObjectFieldNode
- * | DirectiveNode
- * | ListTypeNode
- * | NonNullTypeNode.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\AST\NodeTest
- */
-abstract class Node implements \JsonSerializable
-{
- public ?Location $loc = null;
-
- public string $kind;
-
- /** @param array<string, mixed> $vars */
- public function __construct(array $vars)
- {
- Utils::assign($this, $vars);
- }
-
- /**
- * Returns a clone of this instance and all its children, except Location $loc.
- *
- * @throws \JsonException
- * @throws InvariantViolation
- *
- * @return static
- */
- public function cloneDeep(): self
- {
- return static::cloneValue($this);
- }
-
- /**
- * @template TNode of Node
- * @template TCloneable of TNode|NodeList<TNode>|Location|string
- *
- * @phpstan-param TCloneable $value
- *
- * @throws \JsonException
- * @throws InvariantViolation
- *
- * @phpstan-return TCloneable
- */
- protected static function cloneValue($value)
- {
- if ($value instanceof self) {
- $cloned = clone $value;
- foreach (get_object_vars($cloned) as $prop => $propValue) {
- $cloned->{$prop} = static::cloneValue($propValue); // @phpstan-ignore argument.templateType
- }
-
- return $cloned;
- }
-
- if ($value instanceof NodeList) {
- /**
- * @phpstan-var TCloneable
- *
- * @phpstan-ignore varTag.nativeType (PHPStan is strict about template types and sees NodeList<TNode> as potentially different from TCloneable)
- */
- return $value->cloneDeep();
- }
-
- return $value;
- }
-
- /** @throws \JsonException */
- public function __toString(): string
- {
- return json_encode($this, JSON_THROW_ON_ERROR);
- }
-
- /**
- * Improves upon the default serialization by:
- * - excluding null values
- * - excluding large reference values such as @see Location::$source.
- *
- * @return array<string, mixed>
- */
- public function jsonSerialize(): array
- {
- return $this->toArray();
- }
-
- /** @return array<string, mixed> */
- public function toArray(): array
- {
- return self::recursiveToArray($this);
- }
-
- /** @return array<string, mixed> */
- private static function recursiveToArray(Node $node): array
- {
- $result = [];
-
- foreach (get_object_vars($node) as $prop => $propValue) {
- if ($propValue === null) {
- continue;
- }
-
- if ($propValue instanceof NodeList) {
- $converted = [];
- foreach ($propValue as $item) {
- $converted[] = self::recursiveToArray($item);
- }
- } elseif ($propValue instanceof Node) {
- $converted = self::recursiveToArray($propValue);
- } elseif ($propValue instanceof Location) {
- $converted = $propValue->toArray();
- } else {
- $converted = $propValue;
- }
-
- $result[$prop] = $converted;
- }
-
- return $result;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php
deleted file mode 100644
index 8cff5b0ecb3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * Holds constants of possible AST nodes.
- */
-class NodeKind
-{
- // constants from language/kinds.js:
-
- public const NAME = 'Name';
-
- // Document
- public const DOCUMENT = 'Document';
- public const OPERATION_DEFINITION = 'OperationDefinition';
- public const VARIABLE_DEFINITION = 'VariableDefinition';
- public const VARIABLE = 'Variable';
- public const SELECTION_SET = 'SelectionSet';
- public const FIELD = 'Field';
- public const ARGUMENT = 'Argument';
-
- // Fragments
- public const FRAGMENT_SPREAD = 'FragmentSpread';
- public const INLINE_FRAGMENT = 'InlineFragment';
- public const FRAGMENT_DEFINITION = 'FragmentDefinition';
-
- // Values
- public const INT = 'IntValue';
- public const FLOAT = 'FloatValue';
- public const STRING = 'StringValue';
- public const BOOLEAN = 'BooleanValue';
- public const ENUM = 'EnumValue';
- public const NULL = 'NullValue';
- public const LST = 'ListValue';
- public const OBJECT = 'ObjectValue';
- public const OBJECT_FIELD = 'ObjectField';
-
- // Directives
- public const DIRECTIVE = 'Directive';
-
- // Types
- public const NAMED_TYPE = 'NamedType';
- public const LIST_TYPE = 'ListType';
- public const NON_NULL_TYPE = 'NonNullType';
-
- // Type System Definitions
- public const SCHEMA_DEFINITION = 'SchemaDefinition';
- public const OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition';
-
- // Type Definitions
- public const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
- public const OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition';
- public const FIELD_DEFINITION = 'FieldDefinition';
- public const INPUT_VALUE_DEFINITION = 'InputValueDefinition';
- public const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition';
- public const UNION_TYPE_DEFINITION = 'UnionTypeDefinition';
- public const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
- public const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
- public const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';
-
- // Type Extensions
- public const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
- public const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
- public const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
- public const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
- public const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
- public const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
-
- // Directive Definitions
- public const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
-
- // Type System Extensions
- public const SCHEMA_EXTENSION = 'SchemaExtension';
-
- public const CLASS_MAP = [
- self::NAME => NameNode::class,
-
- // Document
- self::DOCUMENT => DocumentNode::class,
- self::OPERATION_DEFINITION => OperationDefinitionNode::class,
- self::VARIABLE_DEFINITION => VariableDefinitionNode::class,
- self::VARIABLE => VariableNode::class,
- self::SELECTION_SET => SelectionSetNode::class,
- self::FIELD => FieldNode::class,
- self::ARGUMENT => ArgumentNode::class,
-
- // Fragments
- self::FRAGMENT_SPREAD => FragmentSpreadNode::class,
- self::INLINE_FRAGMENT => InlineFragmentNode::class,
- self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
-
- // Values
- self::INT => IntValueNode::class,
- self::FLOAT => FloatValueNode::class,
- self::STRING => StringValueNode::class,
- self::BOOLEAN => BooleanValueNode::class,
- self::ENUM => EnumValueNode::class,
- self::NULL => NullValueNode::class,
- self::LST => ListValueNode::class,
- self::OBJECT => ObjectValueNode::class,
- self::OBJECT_FIELD => ObjectFieldNode::class,
-
- // Directives
- self::DIRECTIVE => DirectiveNode::class,
-
- // Types
- self::NAMED_TYPE => NamedTypeNode::class,
- self::LIST_TYPE => ListTypeNode::class,
- self::NON_NULL_TYPE => NonNullTypeNode::class,
-
- // Type System Definitions
- self::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
- self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
-
- // Type Definitions
- self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
- self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
- self::FIELD_DEFINITION => FieldDefinitionNode::class,
- self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
- self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
- self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
- self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
- self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
- self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class,
-
- // Type Extensions
- self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
- self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
- self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
- self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
- self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
- self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
-
- // Directive Definitions
- self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class,
- ];
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php
deleted file mode 100644
index ade50d309b1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-
-/**
- * @template T of Node
- *
- * @phpstan-implements \ArrayAccess<array-key, T>
- * @phpstan-implements \IteratorAggregate<array-key, T>
- */
-class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable
-{
- /**
- * @var array<Node|array>
- *
- * @phpstan-var array<T|array<string, mixed>>
- */
- private array $nodes;
-
- /**
- * @param array<Node|array> $nodes
- *
- * @phpstan-param array<T|array<string, mixed>> $nodes
- */
- public function __construct(array $nodes)
- {
- $this->nodes = $nodes;
- }
-
- /** @param int|string $offset */
- #[\ReturnTypeWillChange]
- public function offsetExists($offset): bool
- {
- return isset($this->nodes[$offset]);
- }
-
- /**
- * @param int|string $offset
- *
- * @phpstan-return T
- */
- #[\ReturnTypeWillChange]
- public function offsetGet($offset): Node
- {
- $item = $this->nodes[$offset];
-
- if (is_array($item)) {
- // @phpstan-ignore-next-line not really possible to express the correctness of this in PHP
- return $this->nodes[$offset] = AST::fromArray($item);
- }
-
- return $item;
- }
-
- /**
- * @param int|string|null $offset
- * @param Node|array<string, mixed> $value
- *
- * @phpstan-param T|array<string, mixed> $value
- *
- * @throws \JsonException
- * @throws InvariantViolation
- */
- #[\ReturnTypeWillChange]
- public function offsetSet($offset, $value): void
- {
- if (is_array($value)) {
- /** @phpstan-var T $value */
- $value = AST::fromArray($value);
- }
-
- // Happens when a Node is pushed via []=
- if ($offset === null) {
- $this->nodes[] = $value;
-
- return;
- }
-
- $this->nodes[$offset] = $value;
- }
-
- /** @param int|string $offset */
- #[\ReturnTypeWillChange]
- public function offsetUnset($offset): void
- {
- unset($this->nodes[$offset]);
- }
-
- public function getIterator(): \Traversable
- {
- foreach ($this->nodes as $key => $_) {
- yield $key => $this->offsetGet($key);
- }
- }
-
- public function count(): int
- {
- return count($this->nodes);
- }
-
- /**
- * Remove a portion of the NodeList and replace it with something else.
- *
- * @param T|iterable<T>|null $replacement
- *
- * @phpstan-return NodeList<T> the NodeList with the extracted elements
- */
- public function splice(int $offset, int $length, $replacement = null): NodeList
- {
- if (is_iterable($replacement) && ! is_array($replacement)) {
- $replacement = iterator_to_array($replacement);
- }
-
- return new NodeList(
- array_splice($this->nodes, $offset, $length, $replacement)
- );
- }
-
- /**
- * @phpstan-param iterable<array-key, T> $list
- *
- * @phpstan-return NodeList<T>
- */
- public function merge(iterable $list): NodeList
- {
- if (! is_array($list)) {
- $list = iterator_to_array($list);
- }
-
- return new NodeList(array_merge($this->nodes, $list));
- }
-
- /** Resets the keys of the stored nodes to contiguous numeric indexes. */
- public function reindex(): void
- {
- $this->nodes = array_values($this->nodes);
- }
-
- /**
- * Returns a clone of this instance and all its children, except Location $loc.
- *
- * @throws \JsonException
- * @throws InvariantViolation
- *
- * @return static<T>
- */
- public function cloneDeep(): self
- {
- /** @var array<T> $empty */
- $empty = [];
- $cloned = new static($empty);
- foreach ($this->getIterator() as $key => $node) {
- $cloned[$key] = $node->cloneDeep();
- }
-
- return $cloned;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php
deleted file mode 100644
index 5fbd29d5778..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class NonNullTypeNode extends Node implements TypeNode
-{
- public string $kind = NodeKind::NON_NULL_TYPE;
-
- /** @var NamedTypeNode|ListTypeNode */
- public TypeNode $type;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php
deleted file mode 100644
index a28d08e1680..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class NullValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::NULL;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php
deleted file mode 100644
index cf024840369..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ObjectFieldNode extends Node
-{
- public string $kind = NodeKind::OBJECT_FIELD;
-
- public NameNode $name;
-
- /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */
- public ValueNode $value;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php
deleted file mode 100644
index 294bd0056eb..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::OBJECT_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $interfaces;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<FieldDefinitionNode> */
- public NodeList $fields;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php
deleted file mode 100644
index b0a46cbb24e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::OBJECT_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $interfaces;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<FieldDefinitionNode> */
- public NodeList $fields;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php
deleted file mode 100644
index a2b95d57d45..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ObjectValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::OBJECT;
-
- /** @var NodeList<ObjectFieldNode> */
- public NodeList $fields;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php
deleted file mode 100644
index 03fe8120b93..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * @phpstan-type OperationType 'query'|'mutation'|'subscription'
- */
-class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
-{
- public string $kind = NodeKind::OPERATION_DEFINITION;
-
- public ?NameNode $name = null;
-
- /** @var OperationType */
- public string $operation;
-
- /** @var NodeList<VariableDefinitionNode> */
- public NodeList $variableDefinitions;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public SelectionSetNode $selectionSet;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- $this->variableDefinitions ??= new NodeList([]);
- }
-
- public function getSelectionSet(): SelectionSetNode
- {
- return $this->selectionSet;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php
deleted file mode 100644
index 7e882cfacdd..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * @phpstan-import-type OperationType from OperationDefinitionNode
- */
-class OperationTypeDefinitionNode extends Node
-{
- public string $kind = NodeKind::OPERATION_TYPE_DEFINITION;
-
- /** @var OperationType */
- public string $operation;
-
- public NamedTypeNode $type;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php
deleted file mode 100644
index 1f3b20a8c65..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::SCALAR_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php
deleted file mode 100644
index c65e9e3e44c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::SCALAR_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php
deleted file mode 100644
index 3449e0daca0..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode
-{
- public string $kind = NodeKind::SCHEMA_DEFINITION;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<OperationTypeDefinitionNode> */
- public NodeList $operationTypes;
-
- public ?StringValueNode $description = null;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php
deleted file mode 100644
index 19968f8c10e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class SchemaExtensionNode extends Node implements TypeSystemExtensionNode
-{
- public string $kind = NodeKind::SCHEMA_EXTENSION;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<OperationTypeDefinitionNode> */
- public NodeList $operationTypes;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php
deleted file mode 100644
index d3b54fc4306..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode.
- */
-interface SelectionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php
deleted file mode 100644
index 45e071516d3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class SelectionSetNode extends Node
-{
- public string $kind = NodeKind::SELECTION_SET;
-
- /** @var NodeList<SelectionNode&Node> */
- public NodeList $selections;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php
deleted file mode 100644
index 73796ed2963..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class StringValueNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::STRING;
-
- public string $value;
-
- public bool $block = false;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php
deleted file mode 100644
index 30905861a2c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type TypeDefinitionNode = ScalarTypeDefinitionNode
- * | ObjectTypeDefinitionNode
- * | InterfaceTypeDefinitionNode
- * | UnionTypeDefinitionNode
- * | EnumTypeDefinitionNode
- * | InputObjectTypeDefinitionNode.
- */
-interface TypeDefinitionNode extends TypeSystemDefinitionNode
-{
- public function getName(): NameNode;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php
deleted file mode 100644
index 9153e5f23d4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type TypeExtensionNode =
- * | ScalarTypeExtensionNode
- * | ObjectTypeExtensionNode
- * | InterfaceTypeExtensionNode
- * | UnionTypeExtensionNode
- * | EnumTypeExtensionNode
- * | InputObjectTypeExtensionNode;.
- */
-interface TypeExtensionNode extends TypeSystemExtensionNode
-{
- public function getName(): NameNode;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php
deleted file mode 100644
index d18ca8ba25b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type TypeNode = NamedTypeNode
- * | ListTypeNode
- * | NonNullTypeNode.
- */
-interface TypeNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php
deleted file mode 100644
index ca03605780d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type TypeSystemDefinitionNode =
- * | SchemaDefinitionNode
- * | TypeDefinitionNode
- * | DirectiveDefinitionNode.
- */
-interface TypeSystemDefinitionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php
deleted file mode 100644
index bbd57cf5689..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;.
- */
-interface TypeSystemExtensionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php
deleted file mode 100644
index 23c61ed1df0..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
-{
- public string $kind = NodeKind::UNION_TYPE_DEFINITION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $types;
-
- public ?StringValueNode $description = null;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php
deleted file mode 100644
index 21d320c61fe..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class UnionTypeExtensionNode extends Node implements TypeExtensionNode
-{
- public string $kind = NodeKind::UNION_TYPE_EXTENSION;
-
- public NameNode $name;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- /** @var NodeList<NamedTypeNode> */
- public NodeList $types;
-
- public function getName(): NameNode
- {
- return $this->name;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php
deleted file mode 100644
index a49cf77e5e8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-/**
- * export type ValueNode = VariableNode
- * | NullValueNode
- * | IntValueNode
- * | FloatValueNode
- * | StringValueNode
- * | BooleanValueNode
- * | EnumValueNode
- * | ListValueNode
- * | ObjectValueNode.
- */
-interface ValueNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php
deleted file mode 100644
index 9f3700aefb2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class VariableDefinitionNode extends Node implements DefinitionNode
-{
- public string $kind = NodeKind::VARIABLE_DEFINITION;
-
- public VariableNode $variable;
-
- /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
- public TypeNode $type;
-
- /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
- public ?ValueNode $defaultValue = null;
-
- /** @var NodeList<DirectiveNode> */
- public NodeList $directives;
-
- public function __construct(array $vars)
- {
- parent::__construct($vars);
- $this->directives ??= new NodeList([]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php
deleted file mode 100644
index 2b7571f353e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
-
-class VariableNode extends Node implements ValueNode
-{
- public string $kind = NodeKind::VARIABLE;
-
- public NameNode $name;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php b/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php
deleted file mode 100644
index 16413a52ae0..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\BlockStringTest
- */
-class BlockString
-{
- /**
- * Produces the value of a block string from its parsed raw value, similar to
- * CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc.
- *
- * This implements the Automattic\WooCommerce\Vendor\GraphQL spec's BlockStringValue() static algorithm.
- */
- public static function dedentBlockStringLines(string $rawString): string
- {
- $lines = Utils::splitLines($rawString);
-
- // Remove common indentation from all lines but first.
- $commonIndent = self::getIndentation($rawString);
- $linesLength = count($lines);
-
- if ($commonIndent > 0) {
- for ($i = 1; $i < $linesLength; ++$i) {
- $lines[$i] = mb_substr($lines[$i], $commonIndent);
- }
- }
-
- // Remove leading and trailing blank lines.
- $startLine = 0;
- while ($startLine < $linesLength && self::isBlank($lines[$startLine])) {
- ++$startLine;
- }
-
- $endLine = $linesLength;
- while ($endLine > $startLine && self::isBlank($lines[$endLine - 1])) {
- --$endLine;
- }
-
- // Return a string of the lines joined with U+000A.
- return implode("\n", array_slice($lines, $startLine, $endLine - $startLine));
- }
-
- private static function isBlank(string $str): bool
- {
- $strLength = mb_strlen($str);
- for ($i = 0; $i < $strLength; ++$i) {
- if ($str[$i] !== ' ' && $str[$i] !== '\t') {
- return false;
- }
- }
-
- return true;
- }
-
- public static function getIndentation(string $value): int
- {
- $isFirstLine = true;
- $isEmptyLine = true;
- $indent = 0;
- $commonIndent = null;
- $valueLength = mb_strlen($value);
-
- for ($i = 0; $i < $valueLength; ++$i) {
- switch (Utils::charCodeAt($value, $i)) {
- case 13: // \r
- if (Utils::charCodeAt($value, $i + 1) === 10) {
- ++$i; // skip \r\n as one symbol
- }
- // falls through
- // no break
- case 10: // \n
- $isFirstLine = false;
- $isEmptyLine = true;
- $indent = 0;
- break;
- case 9: // \t
- case 32: // <space>
- ++$indent;
- break;
- default:
- if (
- $isEmptyLine
- && ! $isFirstLine
- && ($commonIndent === null || $indent < $commonIndent)
- ) {
- $commonIndent = $indent;
- }
-
- $isEmptyLine = false;
- }
- }
-
- return $commonIndent ?? 0;
- }
-
- /**
- * Print a block string in the indented block form by adding a leading and
- * trailing blank line. However, if a block string starts with whitespace and is
- * a single-line, adding a leading blank line would strip that whitespace.
- */
- public static function print(string $value): string
- {
- $escapedValue = str_replace('"""', '\\"""', $value);
-
- // Expand a block string's raw value into independent lines.
- $lines = Utils::splitLines($escapedValue);
- $isSingleLine = count($lines) === 1;
-
- // If common indentation is found we can fix some of those cases by adding leading new line
- $forceLeadingNewLine = count($lines) > 1;
- foreach ($lines as $i => $line) {
- if ($i === 0) {
- continue;
- }
-
- if ($line !== '' && preg_match('/^\s/', $line) !== 1) {
- $forceLeadingNewLine = false;
- }
- }
-
- // Trailing triple quotes just looks confusing but doesn't force trailing new line
- $hasTrailingTripleQuotes = preg_match('/\\\\"""$/', $escapedValue) === 1;
-
- // Trailing quote (single or double) or slash forces trailing new line
- $hasTrailingQuote = preg_match('/"$/', $value) === 1 && ! $hasTrailingTripleQuotes;
- $hasTrailingSlash = preg_match('/\\\\$/', $value) === 1;
- $forceTrailingNewline = $hasTrailingQuote || $hasTrailingSlash;
-
- // add leading and trailing new lines only if it improves readability
- $printAsMultipleLines = ! $isSingleLine
- || mb_strlen($value) > 70
- || $forceTrailingNewline
- || $forceLeadingNewLine
- || $hasTrailingTripleQuotes;
-
- $result = '';
-
- // Format a multi-line block quote to account for leading space.
- $skipLeadingNewLine = $isSingleLine && preg_match('/^\s/', $value) === 1;
- if (($printAsMultipleLines && ! $skipLeadingNewLine) || $forceLeadingNewLine) {
- $result .= "\n";
- }
-
- $result .= $escapedValue;
- if ($printAsMultipleLines) {
- $result .= "\n";
- }
-
- return '"""' . $result . '"""';
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php
deleted file mode 100644
index bac355962b8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-/**
- * Enumeration of available directive locations.
- */
-class DirectiveLocation
-{
- public const QUERY = 'QUERY';
- public const MUTATION = 'MUTATION';
- public const SUBSCRIPTION = 'SUBSCRIPTION';
- public const FIELD = 'FIELD';
- public const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
- public const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
- public const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
- public const VARIABLE_DEFINITION = 'VARIABLE_DEFINITION';
-
- public const EXECUTABLE_LOCATIONS = [
- self::QUERY => self::QUERY,
- self::MUTATION => self::MUTATION,
- self::SUBSCRIPTION => self::SUBSCRIPTION,
- self::FIELD => self::FIELD,
- self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
- self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
- self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
- self::VARIABLE_DEFINITION => self::VARIABLE_DEFINITION,
- ];
-
- public const SCHEMA = 'SCHEMA';
- public const SCALAR = 'SCALAR';
- public const OBJECT = 'OBJECT';
- public const FIELD_DEFINITION = 'FIELD_DEFINITION';
- public const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
- public const IFACE = 'INTERFACE';
- public const UNION = 'UNION';
- public const ENUM = 'ENUM';
- public const ENUM_VALUE = 'ENUM_VALUE';
- public const INPUT_OBJECT = 'INPUT_OBJECT';
- public const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
-
- public const TYPE_SYSTEM_LOCATIONS = [
- self::SCHEMA => self::SCHEMA,
- self::SCALAR => self::SCALAR,
- self::OBJECT => self::OBJECT,
- self::FIELD_DEFINITION => self::FIELD_DEFINITION,
- self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
- self::IFACE => self::IFACE,
- self::UNION => self::UNION,
- self::ENUM => self::ENUM,
- self::ENUM_VALUE => self::ENUM_VALUE,
- self::INPUT_OBJECT => self::INPUT_OBJECT,
- self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
- ];
-
- public const LOCATIONS = self::EXECUTABLE_LOCATIONS + self::TYPE_SYSTEM_LOCATIONS;
-
- public static function has(string $name): bool
- {
- return isset(self::LOCATIONS[$name]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php
deleted file mode 100644
index 5430d0634c5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php
+++ /dev/null
@@ -1,743 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * A lexer is a stateful stream generator, it returns the next token in the Source when advanced.
- * Assuming the source is valid, the final returned token will be EOF,
- * after which the lexer will repeatedly return the same EOF token whenever called.
- *
- * Algorithm is O(N) both on memory and time.
- *
- * @phpstan-import-type ParserOptions from Parser
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\LexerTest
- */
-class Lexer
-{
- // https://spec.graphql.org/October2021/#sec-Punctuators
- private const TOKEN_BANG = 33;
- private const TOKEN_DOLLAR = 36;
- private const TOKEN_AMP = 38;
- private const TOKEN_PAREN_L = 40;
- private const TOKEN_PAREN_R = 41;
- private const TOKEN_DOT = 46;
- private const TOKEN_COLON = 58;
- private const TOKEN_EQUALS = 61;
- private const TOKEN_AT = 64;
- private const TOKEN_BRACKET_L = 91;
- private const TOKEN_BRACKET_R = 93;
- private const TOKEN_BRACE_L = 123;
- private const TOKEN_PIPE = 124;
- private const TOKEN_BRACE_R = 125;
-
- public Source $source;
-
- /** @phpstan-var ParserOptions */
- public array $options;
-
- /** The previously focused non-ignored token. */
- public Token $lastToken;
-
- /** The currently focused non-ignored token. */
- public Token $token;
-
- /** The (1-indexed) line containing the current token. */
- public int $line = 1;
-
- /** The character offset at which the current line begins. */
- public int $lineStart = 0;
-
- /** Current cursor position for UTF8 encoding of the source. */
- private int $position = 0;
-
- /** Current cursor position for ASCII representation of the source. */
- private int $byteStreamPosition = 0;
-
- /** @phpstan-param ParserOptions $options */
- public function __construct(Source $source, array $options = [])
- {
- $startOfFileToken = new Token(Token::SOF, 0, 0, 0, 0);
-
- $this->source = $source;
- $this->options = $options;
- $this->lastToken = $startOfFileToken;
- $this->token = $startOfFileToken;
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- public function advance(): Token
- {
- $this->lastToken = $this->token;
-
- return $this->token = $this->lookahead();
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- public function lookahead(): Token
- {
- $token = $this->token;
- if ($token->kind !== Token::EOF) {
- do {
- $token = $token->next ?? ($token->next = $this->readToken($token));
- } while ($token->kind === Token::COMMENT);
- }
-
- return $token;
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function readToken(Token $prev): Token
- {
- $bodyLength = $this->source->length;
-
- $this->positionAfterWhitespace();
- $position = $this->position;
-
- $line = $this->line;
- $col = 1 + $position - $this->lineStart;
-
- if ($position >= $bodyLength) {
- return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
- }
-
- // Read next char and advance string cursor:
- [, $code, $bytes] = $this->readChar(true);
-
- switch ($code) {
- case self::TOKEN_BANG: // !
- return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev);
- case 35: // #
- $this->moveStringCursor(-1, -1 * $bytes);
-
- return $this->readComment($line, $col, $prev);
- case self::TOKEN_DOLLAR: // $
- return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_AMP: // &
- return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_PAREN_L: // (
- return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_PAREN_R: // )
- return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_DOT: // .
- [, $charCode1] = $this->readChar(true);
- [, $charCode2] = $this->readChar(true);
-
- if ($charCode1 === self::TOKEN_DOT && $charCode2 === self::TOKEN_DOT) {
- return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev);
- }
-
- break;
- case self::TOKEN_COLON: // :
- return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_EQUALS: // =
- return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_AT: // @
- return new Token(Token::AT, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_BRACKET_L: // [
- return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_BRACKET_R: // ]
- return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_BRACE_L: // {
- return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_PIPE: // |
- return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev);
- case self::TOKEN_BRACE_R: // }
- return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev);
- // A-Z
- case 65:
- case 66:
- case 67:
- case 68:
- case 69:
- case 70:
- case 71:
- case 72:
- case 73:
- case 74:
- case 75:
- case 76:
- case 77:
- case 78:
- case 79:
- case 80:
- case 81:
- case 82:
- case 83:
- case 84:
- case 85:
- case 86:
- case 87:
- case 88:
- case 89:
- case 90:
- // _
- case 95:
- // a-z
- case 97:
- case 98:
- case 99:
- case 100:
- case 101:
- case 102:
- case 103:
- case 104:
- case 105:
- case 106:
- case 107:
- case 108:
- case 109:
- case 110:
- case 111:
- case 112:
- case 113:
- case 114:
- case 115:
- case 116:
- case 117:
- case 118:
- case 119:
- case 120:
- case 121:
- case 122:
- return $this->moveStringCursor(-1, -1 * $bytes)
- ->readName($line, $col, $prev);
- // -
- case 45:
- // 0-9
- case 48:
- case 49:
- case 50:
- case 51:
- case 52:
- case 53:
- case 54:
- case 55:
- case 56:
- case 57:
- return $this->moveStringCursor(-1, -1 * $bytes)
- ->readNumber($line, $col, $prev);
- // "
- case 34:
- [, $nextCode] = $this->readChar();
- [, $nextNextCode] = $this->moveStringCursor(1, 1)
- ->readChar();
-
- if ($nextCode === 34 && $nextNextCode === 34) {
- return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
- ->readBlockString($line, $col, $prev);
- }
-
- return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
- ->readString($line, $col, $prev);
- }
-
- throw new SyntaxError($this->source, $position, $this->unexpectedCharacterMessage($code));
- }
-
- /** @throws \JsonException */
- private function unexpectedCharacterMessage(?int $code): string
- {
- // SourceCharacter
- if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
- return 'Cannot contain the invalid character ' . Utils::printCharCode($code);
- }
-
- if ($code === 39) {
- return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
- }
-
- return 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.';
- }
-
- /**
- * Reads an alphanumeric + underscore name from the source.
- *
- * [_A-Za-z][_0-9A-Za-z]*
- */
- private function readName(int $line, int $col, Token $prev): Token
- {
- $start = $this->position;
- $body = $this->source->body;
- $length = strspn($body, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_', $this->byteStreamPosition);
- $value = substr($body, $this->byteStreamPosition, $length);
- $this->moveStringCursor($length, $length);
-
- return new Token(
- Token::NAME,
- $start,
- $this->position,
- $line,
- $col,
- $prev,
- $value
- );
- }
-
- /**
- * Reads a number token from the source file, either a float
- * or an int depending on whether a decimal point appears.
- *
- * Int: -?(0|[1-9][0-9]*)
- * Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function readNumber(int $line, int $col, Token $prev): Token
- {
- $value = '';
- $start = $this->position;
- [$char, $code] = $this->readChar();
-
- $isFloat = false;
-
- if ($code === 45) { // -
- $value .= $char;
- [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
- }
-
- // guard against leading zero's
- if ($code === 48) { // 0
- $value .= $char;
- [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
-
- if ($code >= 48 && $code <= 57) {
- throw new SyntaxError($this->source, $this->position, 'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code));
- }
- } else {
- $value .= $this->readDigits();
- [$char, $code] = $this->readChar();
- }
-
- if ($code === 46) { // .
- $isFloat = true;
- $this->moveStringCursor(1, 1);
-
- $value .= $char;
- $value .= $this->readDigits();
- [$char, $code] = $this->readChar();
- }
-
- if ($code === 69 || $code === 101) { // E e
- $isFloat = true;
- $value .= $char;
- [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
-
- if ($code === 43 || $code === 45) { // + -
- $value .= $char;
- $this->moveStringCursor(1, 1);
- }
-
- $value .= $this->readDigits();
- }
-
- return new Token(
- $isFloat ? Token::FLOAT : Token::INT,
- $start,
- $this->position,
- $line,
- $col,
- $prev,
- $value
- );
- }
-
- /**
- * Returns string with all digits + changes current string cursor position to point to the first char after digits.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function readDigits(): string
- {
- [$char, $code] = $this->readChar();
-
- if ($code >= 48 && $code <= 57) { // 0 - 9
- $value = '';
-
- do {
- $value .= $char;
- [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
- } while ($code >= 48 && $code <= 57); // 0 - 9
-
- return $value;
- }
-
- if ($this->position > $this->source->length - 1) {
- $code = null;
- }
-
- throw new SyntaxError($this->source, $this->position, 'Invalid number, expected digit but got: ' . Utils::printCharCode($code));
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function readString(int $line, int $col, Token $prev): Token
- {
- $start = $this->position;
-
- // Skip leading quote and read first string char:
- [$char, $code, $bytes] = $this->moveStringCursor(1, 1)
- ->readChar();
-
- $chunk = '';
- $value = '';
-
- while (! in_array($code, [null, 10, 13], true)) { // not LineTerminator
- if ($code === 34) { // Closing Quote (")
- $value .= $chunk;
-
- // Skip quote
- $this->moveStringCursor(1, 1);
-
- return new Token(
- Token::STRING,
- $start,
- $this->position,
- $line,
- $col,
- $prev,
- $value
- );
- }
-
- $this->assertValidStringCharacterCode($code, $this->position);
- $this->moveStringCursor(1, $bytes);
-
- if ($code === 92) { // \
- $value .= $chunk;
- [, $code] = $this->readChar(true);
-
- switch ($code) {
- case 34:
- $value .= '"';
- break;
- case 47:
- $value .= '/';
- break;
- case 92:
- $value .= '\\';
- break;
- case 98:
- $value .= chr(8); // \b (backspace)
- break;
- case 102:
- $value .= "\f";
- break;
- case 110:
- $value .= "\n";
- break;
- case 114:
- $value .= "\r";
- break;
- case 116:
- $value .= "\t";
- break;
- case 117:
- $position = $this->position;
- [$hex] = $this->readChars(4);
- if (preg_match('/[0-9a-fA-F]{4}/', $hex) !== 1) {
- throw new SyntaxError($this->source, $position - 1, "Invalid character escape sequence: \\u{$hex}");
- }
-
- $code = hexdec($hex);
- assert(is_int($code), 'Since only a single char is read');
-
- // UTF-16 surrogate pair detection and handling.
- $highOrderByte = $code >> 8;
- if ($highOrderByte >= 0xD8 && $highOrderByte <= 0xDF) {
- [$utf16Continuation] = $this->readChars(6);
- if (preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation) !== 1) {
- throw new SyntaxError($this->source, $this->position - 5, 'Invalid UTF-16 trailing surrogate: ' . $utf16Continuation);
- }
-
- $surrogatePairHex = $hex . substr($utf16Continuation, 2, 4);
- $value .= mb_convert_encoding(pack('H*', $surrogatePairHex), 'UTF-8', 'UTF-16');
- break;
- }
-
- $this->assertValidStringCharacterCode($code, $position - 2);
-
- $value .= Utils::chr($code);
- break;
- // null means EOF, will delegate to general handling of unterminated strings
- case null:
- continue 2;
- default:
- $chr = Utils::chr($code);
- throw new SyntaxError($this->source, $this->position - 1, "Invalid character escape sequence: \\{$chr}");
- }
-
- $chunk = '';
- } else {
- $chunk .= $char;
- }
-
- [$char, $code, $bytes] = $this->readChar();
- }
-
- throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
- }
-
- /**
- * Reads a block string token from the source file.
- *
- * """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function readBlockString(int $line, int $col, Token $prev): Token
- {
- $start = $this->position;
-
- // Skip leading quotes and read first string char:
- [$char, $code, $bytes] = $this->moveStringCursor(3, 3)->readChar();
-
- $chunk = '';
- $value = '';
-
- while ($code !== null) {
- // Closing Triple-Quote (""")
- if ($code === 34) {
- // Move 2 quotes
- [, $nextCode] = $this->moveStringCursor(1, 1)->readChar();
- [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
-
- if ($nextCode === 34 && $nextNextCode === 34) {
- $value .= $chunk;
-
- $this->moveStringCursor(1, 1);
-
- return new Token(
- Token::BLOCK_STRING,
- $start,
- $this->position,
- $line,
- $col,
- $prev,
- BlockString::dedentBlockStringLines($value)
- );
- }
-
- // move cursor back to before the first quote
- $this->moveStringCursor(-2, -2);
- }
-
- $this->assertValidBlockStringCharacterCode($code, $this->position);
- $this->moveStringCursor(1, $bytes);
-
- [, $nextCode] = $this->readChar();
- [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
- [, $nextNextNextCode] = $this->moveStringCursor(1, 1)->readChar();
-
- // Escape Triple-Quote (\""")
- if (
- $code === 92
- && $nextCode === 34
- && $nextNextCode === 34
- && $nextNextNextCode === 34
- ) {
- $this->moveStringCursor(1, 1);
- $value .= $chunk . '"""';
- $chunk = '';
- } else {
- // move cursor back to before the first quote
- $this->moveStringCursor(-2, -2);
-
- if ($code === 10) { // new line
- ++$this->line;
- $this->lineStart = $this->position;
- }
-
- $chunk .= $char;
- }
-
- [$char, $code, $bytes] = $this->readChar();
- }
-
- throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function assertValidStringCharacterCode(int $code, int $position): void
- {
- // SourceCharacter
- if ($code < 0x0020 && $code !== 0x0009) {
- $char = Utils::printCharCode($code);
- throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
- }
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function assertValidBlockStringCharacterCode(int $code, int $position): void
- {
- // SourceCharacter
- if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
- $char = Utils::printCharCode($code);
- throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
- }
- }
-
- /**
- * Reads from body starting at startPosition until it finds a non-whitespace
- * or commented character, then places cursor to the position of that character.
- */
- private function positionAfterWhitespace(): void
- {
- while ($this->position < $this->source->length) {
- [, $code, $bytes] = $this->readChar();
-
- // Skip whitespace
- // tab | space | comma | BOM
- if (in_array($code, [9, 32, 44, 0xFEFF], true)) {
- $this->moveStringCursor(1, $bytes);
- } elseif ($code === 10) { // new line
- $this->moveStringCursor(1, $bytes);
- ++$this->line;
- $this->lineStart = $this->position;
- } elseif ($code === 13) { // carriage return
- [, $nextCode, $nextBytes] = $this->moveStringCursor(1, $bytes)->readChar();
-
- if ($nextCode === 10) { // lf after cr
- $this->moveStringCursor(1, $nextBytes);
- }
-
- ++$this->line;
- $this->lineStart = $this->position;
- } else {
- break;
- }
- }
- }
-
- /**
- * Reads a comment token from the source file.
- *
- * #[\u0009\u0020-\uFFFF]*
- */
- private function readComment(int $line, int $col, Token $prev): Token
- {
- $start = $this->position;
- $value = '';
- $bytes = 1;
-
- do {
- [$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar();
- $value .= $char;
- } while (
- $code !== null
- // SourceCharacter but not LineTerminator
- && ($code > 0x001F || $code === 0x0009)
- );
-
- return new Token(
- Token::COMMENT,
- $start,
- $this->position,
- $line,
- $col,
- $prev,
- $value
- );
- }
-
- /**
- * Reads next UTF8Character from the byte stream, starting from $byteStreamPosition.
- *
- * @return array{string, int|null, int}
- */
- private function readChar(bool $advance = false, ?int $byteStreamPosition = null): array
- {
- if ($byteStreamPosition === null) {
- $byteStreamPosition = $this->byteStreamPosition;
- }
-
- $code = null;
- $utf8char = '';
- $bytes = 0;
- $positionOffset = 0;
-
- if (isset($this->source->body[$byteStreamPosition])) {
- $ord = ord($this->source->body[$byteStreamPosition]);
-
- if ($ord < 128) {
- $bytes = 1;
- } elseif ($ord < 224) {
- $bytes = 2;
- } elseif ($ord < 240) {
- $bytes = 3;
- } else {
- $bytes = 4;
- }
-
- for ($pos = $byteStreamPosition; $pos < $byteStreamPosition + $bytes; ++$pos) {
- $utf8char .= $this->source->body[$pos];
- }
-
- $positionOffset = 1;
- $code = $bytes === 1
- ? $ord
- : Utils::ord($utf8char);
- }
-
- if ($advance) {
- $this->moveStringCursor($positionOffset, $bytes);
- }
-
- return [$utf8char, $code, $bytes];
- }
-
- /**
- * Reads next $numberOfChars UTF8 characters from the byte stream.
- *
- * @return array{string, int}
- */
- private function readChars(int $charCount): array
- {
- $result = '';
- $totalBytes = 0;
- $byteOffset = $this->byteStreamPosition;
-
- for ($i = 0; $i < $charCount; ++$i) {
- [$char, $code, $bytes] = $this->readChar(false, $byteOffset);
- $totalBytes += $bytes;
- $byteOffset += $bytes;
- $result .= $char;
- }
-
- $this->moveStringCursor($charCount, $totalBytes);
-
- return [$result, $totalBytes];
- }
-
- /** Moves internal string cursor position. */
- private function moveStringCursor(int $positionOffset, int $byteStreamOffset): self
- {
- $this->position += $positionOffset;
- $this->byteStreamPosition += $byteStreamOffset;
-
- return $this;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php
deleted file mode 100644
index 69d16f39fe4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php
+++ /dev/null
@@ -1,1926 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ExecutableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Location;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-
-/**
- * Parses string containing Automattic\WooCommerce\Vendor\GraphQL query language or [schema definition language](schema-definition-language.md) to Abstract Syntax Tree.
- *
- * @phpstan-type ParserOptions array{
- * noLocation?: bool,
- * allowLegacySDLEmptyFields?: bool,
- * allowLegacySDLImplementsInterfaces?: bool,
- * experimentalFragmentVariables?: bool,
- * recursionLimit?: int<0, max>
- * }
- *
- * - **noLocation**:
- * By default, the parser creates AST nodes that know the location in the source.
- * This configuration flag disables that behavior for performance or testing.
- *
- * - **allowLegacySDLEmptyFields**:
- * If enabled, the parser will parse empty fields sets in the Schema Definition Language.
- * Otherwise, the parser will follow the current specification.
- * This option is provided to ease adoption of the final SDL specification and will be removed in a future major release.
- *
- * - **allowLegacySDLImplementsInterfaces**:
- * If enabled, the parser will parse implemented interfaces with no `&` character between each interface.
- * Otherwise, the parser will follow the current specification.
- * This option is provided to ease adoption of the final SDL specification and will be removed in a future major release.
- *
- * - **experimentalFragmentVariables**:
- * If enabled, the parser will understand and parse variable definitions contained in a fragment definition.
- * They'll be represented in the `variableDefinitions` field of the FragmentDefinitionNode.
- * The syntax is identical to normal, query-defined variables. For example:
- *
- * ```graphql
- * fragment A($var: Boolean = false) on T {
- * ...
- * }
- * ```
- *
- * Note: this feature is experimental and may change or be removed in the future.
- *
- * - **recursionLimit**:
- * Limits the depth of recursion during parsing to prevent stack overflows from deeply nested queries.
- * The counter is shared across `parseSelectionSet`, `parseValueLiteral`, and `parseTypeReference`.
- * Defaults to 256. Set to 0 to disable the limit.
- *
- * Those magic functions allow partial parsing:
- *
- * @method static NameNode name(Source|string $source, ParserOptions $options = [])
- * @method static ExecutableDefinitionNode|TypeSystemDefinitionNode definition(Source|string $source, ParserOptions $options = [])
- * @method static ExecutableDefinitionNode executableDefinition(Source|string $source, ParserOptions $options = [])
- * @method static OperationDefinitionNode operationDefinition(Source|string $source, ParserOptions $options = [])
- * @method static string operationType(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<VariableDefinitionNode> variableDefinitions(Source|string $source, ParserOptions $options = [])
- * @method static VariableDefinitionNode variableDefinition(Source|string $source, ParserOptions $options = [])
- * @method static VariableNode variable(Source|string $source, ParserOptions $options = [])
- * @method static SelectionSetNode selectionSet(Source|string $source, ParserOptions $options = [])
- * @method static mixed selection(Source|string $source, ParserOptions $options = [])
- * @method static FieldNode field(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<ArgumentNode> arguments(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<ArgumentNode> constArguments(Source|string $source, ParserOptions $options = [])
- * @method static ArgumentNode argument(Source|string $source, ParserOptions $options = [])
- * @method static ArgumentNode constArgument(Source|string $source, ParserOptions $options = [])
- * @method static FragmentSpreadNode|InlineFragmentNode fragment(Source|string $source, ParserOptions $options = [])
- * @method static FragmentDefinitionNode fragmentDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NameNode fragmentName(Source|string $source, ParserOptions $options = [])
- * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode valueLiteral(Source|string $source, ParserOptions $options = [])
- * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode constValueLiteral(Source|string $source, ParserOptions $options = [])
- * @method static StringValueNode stringLiteral(Source|string $source, ParserOptions $options = [])
- * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode constValue(Source|string $source, ParserOptions $options = [])
- * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode variableValue(Source|string $source, ParserOptions $options = [])
- * @method static ListValueNode array(Source|string $source, ParserOptions $options = [])
- * @method static ListValueNode constArray(Source|string $source, ParserOptions $options = [])
- * @method static ObjectValueNode object(Source|string $source, ParserOptions $options = [])
- * @method static ObjectValueNode constObject(Source|string $source, ParserOptions $options = [])
- * @method static ObjectFieldNode objectField(Source|string $source, ParserOptions $options = [])
- * @method static ObjectFieldNode constObjectField(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<DirectiveNode> directives(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<DirectiveNode> constDirectives(Source|string $source, ParserOptions $options = [])
- * @method static DirectiveNode directive(Source|string $source, ParserOptions $options = [])
- * @method static DirectiveNode constDirective(Source|string $source, ParserOptions $options = [])
- * @method static ListTypeNode|NamedTypeNode|NonNullTypeNode typeReference(Source|string $source, ParserOptions $options = [])
- * @method static NamedTypeNode namedType(Source|string $source, ParserOptions $options = [])
- * @method static TypeSystemDefinitionNode typeSystemDefinition(Source|string $source, ParserOptions $options = [])
- * @method static StringValueNode|null description(Source|string $source, ParserOptions $options = [])
- * @method static SchemaDefinitionNode schemaDefinition(Source|string $source, ParserOptions $options = [])
- * @method static OperationTypeDefinitionNode operationTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static ScalarTypeDefinitionNode scalarTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static ObjectTypeDefinitionNode objectTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<NamedTypeNode> implementsInterfaces(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<FieldDefinitionNode> fieldsDefinition(Source|string $source, ParserOptions $options = [])
- * @method static FieldDefinitionNode fieldDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<InputValueDefinitionNode> argumentsDefinition(Source|string $source, ParserOptions $options = [])
- * @method static InputValueDefinitionNode inputValueDefinition(Source|string $source, ParserOptions $options = [])
- * @method static InterfaceTypeDefinitionNode interfaceTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static UnionTypeDefinitionNode unionTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<NamedTypeNode> unionMemberTypes(Source|string $source, ParserOptions $options = [])
- * @method static EnumTypeDefinitionNode enumTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<EnumValueDefinitionNode> enumValuesDefinition(Source|string $source, ParserOptions $options = [])
- * @method static EnumValueDefinitionNode enumValueDefinition(Source|string $source, ParserOptions $options = [])
- * @method static InputObjectTypeDefinitionNode inputObjectTypeDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<InputValueDefinitionNode> inputFieldsDefinition(Source|string $source, ParserOptions $options = [])
- * @method static TypeExtensionNode typeExtension(Source|string $source, ParserOptions $options = [])
- * @method static SchemaExtensionNode schemaTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static ScalarTypeExtensionNode scalarTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static ObjectTypeExtensionNode objectTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static InterfaceTypeExtensionNode interfaceTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static UnionTypeExtensionNode unionTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static EnumTypeExtensionNode enumTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static InputObjectTypeExtensionNode inputObjectTypeExtension(Source|string $source, ParserOptions $options = [])
- * @method static DirectiveDefinitionNode directiveDefinition(Source|string $source, ParserOptions $options = [])
- * @method static NodeList<NameNode> directiveLocations(Source|string $source, ParserOptions $options = [])
- * @method static NameNode directiveLocation(Source|string $source, ParserOptions $options = [])
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\ParserTest
- */
-class Parser
-{
- /** @api */
- public const DEFAULT_RECURSION_LIMIT = 256;
-
- /**
- * Given a Automattic\WooCommerce\Vendor\GraphQL source, parses it into a `Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode`.
- *
- * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
- *
- * @param Source|string $source
- *
- * @phpstan-param ParserOptions $options
- *
- * @api
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- public static function parse($source, array $options = []): DocumentNode
- {
- return (new self($source, $options))->parseDocument();
- }
-
- /**
- * Given a string containing a Automattic\WooCommerce\Vendor\GraphQL value (ex. `[42]`), parse the AST for that value.
- *
- * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
- *
- * This is useful within tools that operate upon Automattic\WooCommerce\Vendor\GraphQL Values directly and
- * in isolation of complete Automattic\WooCommerce\Vendor\GraphQL documents.
- *
- * Consider providing the results to the utility function: `Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::valueFromAST()`.
- *
- * @param Source|string $source
- *
- * @phpstan-param ParserOptions $options
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode
- *
- * @api
- */
- public static function parseValue($source, array $options = [])
- {
- $parser = new Parser($source, $options);
- $parser->expect(Token::SOF);
- $value = $parser->parseValueLiteral(false);
- $parser->expect(Token::EOF);
-
- return $value;
- }
-
- /**
- * Given a string containing a Automattic\WooCommerce\Vendor\GraphQL Type (ex. `[Int!]`), parse the AST for that type.
- *
- * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
- *
- * This is useful within tools that operate upon Automattic\WooCommerce\Vendor\GraphQL Types directly and
- * in isolation of complete Automattic\WooCommerce\Vendor\GraphQL documents.
- *
- * Consider providing the results to the utility function: `Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::typeFromAST()`.
- *
- * @param Source|string $source
- *
- * @phpstan-param ParserOptions $options
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return ListTypeNode|NamedTypeNode|NonNullTypeNode
- *
- * @api
- */
- public static function parseType($source, array $options = [])
- {
- $parser = new Parser($source, $options);
- $parser->expect(Token::SOF);
- $type = $parser->parseTypeReference();
- $parser->expect(Token::EOF);
-
- return $type;
- }
-
- /**
- * Parse partial source by delegating calls to the internal parseX methods.
- *
- * @phpstan-param array{string, ParserOptions} $arguments
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return Node|NodeList<Node>
- */
- public static function __callStatic(string $name, array $arguments)
- {
- $parser = new Parser(...$arguments);
- $parser->expect(Token::SOF);
-
- switch ($name) {
- case 'arguments':
- $parsed = $parser->parseArguments(false);
- break;
- case 'valueLiteral':
- $parsed = $parser->parseValueLiteral(false);
- break;
- case 'array':
- $parsed = $parser->parseArray(false);
- break;
- case 'object':
- $parsed = $parser->parseObject(false);
- break;
- case 'objectField':
- $parsed = $parser->parseObjectField(false);
- break;
- case 'directives':
- $parsed = $parser->parseDirectives(false);
- break;
- case 'directive':
- $parsed = $parser->parseDirective(false);
- break;
- case 'constArguments':
- $parsed = $parser->parseArguments(true);
- break;
- case 'constValueLiteral':
- $parsed = $parser->parseValueLiteral(true);
- break;
- case 'constArray':
- $parsed = $parser->parseArray(true);
- break;
- case 'constObject':
- $parsed = $parser->parseObject(true);
- break;
- case 'constObjectField':
- $parsed = $parser->parseObjectField(true);
- break;
- case 'constDirectives':
- $parsed = $parser->parseDirectives(true);
- break;
- case 'constDirective':
- $parsed = $parser->parseDirective(true);
- break;
- default:
- $parsed = $parser->{'parse' . $name}();
- }
-
- $parser->expect(Token::EOF);
-
- return $parsed;
- }
-
- private Lexer $lexer;
-
- private int $recursionDepth = 0;
-
- private int $recursionLimit;
-
- /**
- * @param Source|string $source
- *
- * @phpstan-param ParserOptions $options
- */
- public function __construct($source, array $options = [])
- {
- $sourceObj = $source instanceof Source
- ? $source
- : new Source($source);
- $this->lexer = new Lexer($sourceObj, $options);
- $this->recursionLimit = $options['recursionLimit'] ?? self::DEFAULT_RECURSION_LIMIT;
- }
-
- /**
- * Returns a location object, used to identify the place in
- * the source that created a given parsed object.
- */
- private function loc(Token $startToken): ?Location
- {
- if (! ($this->lexer->options['noLocation'] ?? false)) {
- return new Location($startToken, $this->lexer->lastToken, $this->lexer->source);
- }
-
- return null;
- }
-
- /** @throws SyntaxError */
- private function increaseRecursionDepth(): void
- {
- if ($this->recursionLimit > 0 && $this->recursionDepth >= $this->recursionLimit) {
- throw new SyntaxError($this->lexer->source, $this->lexer->token->start, "Recursion depth limit of {$this->recursionLimit} exceeded");
- }
-
- ++$this->recursionDepth;
- }
-
- /** Determines if the next token is of a given kind. */
- private function peek(string $kind): bool
- {
- return $this->lexer->token->kind === $kind;
- }
-
- /**
- * If the next token is of the given kind, return true after advancing
- * the parser. Otherwise, do not change the parser state and return false.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function skip(string $kind): bool
- {
- $match = $this->lexer->token->kind === $kind;
-
- if ($match) {
- $this->lexer->advance();
- }
-
- return $match;
- }
-
- /**
- * If the next token is of the given kind, return that token after advancing
- * the parser. Otherwise, do not change the parser state and return false.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function expect(string $kind): Token
- {
- $token = $this->lexer->token;
-
- if ($token->kind === $kind) {
- $this->lexer->advance();
-
- return $token;
- }
-
- throw new SyntaxError($this->lexer->source, $token->start, "Expected {$kind}, found {$token->getDescription()}");
- }
-
- /**
- * If the next token is a keyword with the given value, advance the lexer.
- * Otherwise, throw an error.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function expectKeyword(string $value): void
- {
- $token = $this->lexer->token;
- if ($token->kind !== Token::NAME || $token->value !== $value) {
- throw new SyntaxError($this->lexer->source, $token->start, "Expected \"{$value}\", found {$token->getDescription()}");
- }
-
- $this->lexer->advance();
- }
-
- /**
- * If the next token is a given keyword, return "true" after advancing
- * the lexer. Otherwise, do not change the parser state and return "false".
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function expectOptionalKeyword(string $value): bool
- {
- $token = $this->lexer->token;
- if ($token->kind === Token::NAME && $token->value === $value) {
- $this->lexer->advance();
-
- return true;
- }
-
- return false;
- }
-
- private function unexpected(?Token $atToken = null): SyntaxError
- {
- $token = $atToken ?? $this->lexer->token;
-
- return new SyntaxError($this->lexer->source, $token->start, 'Unexpected ' . $token->getDescription());
- }
-
- /**
- * Returns a possibly empty list of parse nodes, determined by
- * the parseFn. This list begins with a lex token of openKind
- * and ends with a lex token of closeKind. Advances the parser
- * to the next lex token after the closing token.
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<Node>
- */
- private function any(string $openKind, callable $parseFn, string $closeKind): NodeList
- {
- $this->expect($openKind);
-
- $nodes = [];
- while (! $this->skip($closeKind)) {
- $nodes[] = $parseFn($this);
- }
-
- return new NodeList($nodes);
- }
-
- /**
- * Returns a non-empty list of parse nodes, determined by
- * the parseFn. This list begins with a lex token of openKind
- * and ends with a lex token of closeKind. Advances the parser
- * to the next lex token after the closing token.
- *
- * @template TNode of Node
- *
- * @param callable(self): TNode $parseFn
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<TNode>
- */
- private function many(string $openKind, callable $parseFn, string $closeKind): NodeList
- {
- $this->expect($openKind);
-
- $nodes = [$parseFn($this)];
- while (! $this->skip($closeKind)) {
- $nodes[] = $parseFn($this);
- }
-
- return new NodeList($nodes);
- }
-
- /**
- * Converts a name lex token into a name parse node.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseName(): NameNode
- {
- $token = $this->expect(Token::NAME);
-
- return new NameNode([
- 'value' => $token->value,
- 'loc' => $this->loc($token),
- ]);
- }
-
- /**
- * Implements the parsing rules in the Document section.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseDocument(): DocumentNode
- {
- $start = $this->lexer->token;
-
- return new DocumentNode([
- 'definitions' => $this->many(
- Token::SOF,
- fn (): DefinitionNode => $this->parseDefinition(),
- Token::EOF
- ),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return DefinitionNode&Node
- */
- private function parseDefinition(): DefinitionNode
- {
- if ($this->peek(Token::NAME)) {
- switch ($this->lexer->token->value) {
- case 'query':
- case 'mutation':
- case 'subscription':
- case 'fragment':
- return $this->parseExecutableDefinition();
-
- // Note: The schema definition language is an experimental addition.
- case 'schema':
- case 'scalar':
- case 'type':
- case 'interface':
- case 'union':
- case 'enum':
- case 'input':
- case 'directive':
- // Note: The schema definition language is an experimental addition.
- return $this->parseTypeSystemDefinition();
-
- case 'extend':
- return $this->parseTypeSystemExtension();
- }
- } elseif ($this->peek(Token::BRACE_L)) {
- return $this->parseExecutableDefinition();
- } elseif ($this->peekDescription()) {
- // Note: The schema definition language is an experimental addition.
- return $this->parseTypeSystemDefinition();
- }
-
- throw $this->unexpected();
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return ExecutableDefinitionNode&Node
- */
- private function parseExecutableDefinition(): ExecutableDefinitionNode
- {
- if ($this->peek(Token::NAME)) {
- switch ($this->lexer->token->value) {
- case 'query':
- case 'mutation':
- case 'subscription':
- return $this->parseOperationDefinition();
-
- case 'fragment':
- return $this->parseFragmentDefinition();
- }
- } elseif ($this->peek(Token::BRACE_L)) {
- return $this->parseOperationDefinition();
- }
-
- throw $this->unexpected();
- }
-
- // Implements the parsing rules in the Operations section.
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseOperationDefinition(): OperationDefinitionNode
- {
- $start = $this->lexer->token;
- if ($this->peek(Token::BRACE_L)) {
- return new OperationDefinitionNode([
- 'name' => null,
- 'operation' => 'query',
- 'variableDefinitions' => new NodeList([]),
- 'directives' => new NodeList([]),
- 'selectionSet' => $this->parseSelectionSet(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- $operation = $this->parseOperationType();
-
- $name = null;
- if ($this->peek(Token::NAME)) {
- $name = $this->parseName();
- }
-
- return new OperationDefinitionNode([
- 'name' => $name,
- 'operation' => $operation,
- 'variableDefinitions' => $this->parseVariableDefinitions(),
- 'directives' => $this->parseDirectives(false),
- 'selectionSet' => $this->parseSelectionSet(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseOperationType(): string
- {
- $operationToken = $this->expect(Token::NAME);
- switch ($operationToken->value) {
- case 'query':
- return 'query';
-
- case 'mutation':
- return 'mutation';
-
- case 'subscription':
- return 'subscription';
- }
-
- throw $this->unexpected($operationToken);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<VariableDefinitionNode>
- */
- private function parseVariableDefinitions(): NodeList
- {
- return $this->peek(Token::PAREN_L)
- ? $this->many(
- Token::PAREN_L,
- fn (): VariableDefinitionNode => $this->parseVariableDefinition(),
- Token::PAREN_R
- )
- : new NodeList([]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseVariableDefinition(): VariableDefinitionNode
- {
- $start = $this->lexer->token;
- $var = $this->parseVariable();
-
- $this->expect(Token::COLON);
- $type = $this->parseTypeReference();
-
- return new VariableDefinitionNode([
- 'variable' => $var,
- 'type' => $type,
- 'defaultValue' => $this->skip(Token::EQUALS)
- ? $this->parseValueLiteral(true)
- : null,
- 'directives' => $this->parseDirectives(true),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseVariable(): VariableNode
- {
- $start = $this->lexer->token;
- $this->expect(Token::DOLLAR);
-
- return new VariableNode([
- 'name' => $this->parseName(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseSelectionSet(): SelectionSetNode
- {
- $this->increaseRecursionDepth();
-
- try {
- $start = $this->lexer->token;
-
- return new SelectionSetNode(
- [
- 'selections' => $this->many(
- Token::BRACE_L,
- fn (): SelectionNode => $this->parseSelection(),
- Token::BRACE_R
- ),
- 'loc' => $this->loc($start),
- ]
- );
- } finally {
- --$this->recursionDepth;
- }
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return SelectionNode&Node
- */
- private function parseSelection(): SelectionNode
- {
- return $this->peek(Token::SPREAD)
- ? $this->parseFragment()
- : $this->parseField();
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseField(): FieldNode
- {
- $start = $this->lexer->token;
- $nameOrAlias = $this->parseName();
-
- if ($this->skip(Token::COLON)) {
- $alias = $nameOrAlias;
- $name = $this->parseName();
- } else {
- $alias = null;
- $name = $nameOrAlias;
- }
-
- return new FieldNode([
- 'name' => $name,
- 'alias' => $alias,
- 'arguments' => $this->parseArguments(false),
- 'directives' => $this->parseDirectives(false),
- 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<ArgumentNode>
- */
- private function parseArguments(bool $isConst): NodeList
- {
- $parseFn = $isConst
- ? fn (): ArgumentNode => $this->parseConstArgument()
- : fn (): ArgumentNode => $this->parseArgument();
-
- return $this->peek(Token::PAREN_L)
- ? $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R)
- : new NodeList([]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseArgument(): ArgumentNode
- {
- $start = $this->lexer->token;
- $name = $this->parseName();
-
- $this->expect(Token::COLON);
- $value = $this->parseValueLiteral(false);
-
- return new ArgumentNode([
- 'name' => $name,
- 'value' => $value,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseConstArgument(): ArgumentNode
- {
- $start = $this->lexer->token;
- $name = $this->parseName();
-
- $this->expect(Token::COLON);
- $value = $this->parseConstValue();
-
- return new ArgumentNode([
- 'name' => $name,
- 'value' => $value,
- 'loc' => $this->loc($start),
- ]);
- }
-
- // Implements the parsing rules in the Fragments section.
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return FragmentSpreadNode|InlineFragmentNode
- */
- private function parseFragment(): SelectionNode
- {
- $start = $this->lexer->token;
- $this->expect(Token::SPREAD);
-
- $hasTypeCondition = $this->expectOptionalKeyword('on');
- if (! $hasTypeCondition && $this->peek(Token::NAME)) {
- return new FragmentSpreadNode([
- 'name' => $this->parseFragmentName(),
- 'directives' => $this->parseDirectives(false),
- 'loc' => $this->loc($start),
- ]);
- }
-
- return new InlineFragmentNode([
- 'typeCondition' => $hasTypeCondition ? $this->parseNamedType() : null,
- 'directives' => $this->parseDirectives(false),
- 'selectionSet' => $this->parseSelectionSet(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseFragmentDefinition(): FragmentDefinitionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('fragment');
-
- $name = $this->parseFragmentName();
-
- // Experimental support for defining variables within fragments changes
- // the grammar of FragmentDefinition:
- // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
- $variableDefinitions = isset($this->lexer->options['experimentalFragmentVariables'])
- ? $this->parseVariableDefinitions()
- : null;
-
- $this->expectKeyword('on');
- $typeCondition = $this->parseNamedType();
-
- return new FragmentDefinitionNode([
- 'name' => $name,
- 'variableDefinitions' => $variableDefinitions,
- 'typeCondition' => $typeCondition,
- 'directives' => $this->parseDirectives(false),
- 'selectionSet' => $this->parseSelectionSet(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseFragmentName(): NameNode
- {
- if ($this->lexer->token->value === 'on') {
- throw $this->unexpected();
- }
-
- return $this->parseName();
- }
-
- // Implements the parsing rules in the Values section.
-
- /**
- * Value[Const] :
- * - [~Const] Variable
- * - IntValue
- * - FloatValue
- * - StringValue
- * - BooleanValue
- * - NullValue
- * - EnumValue
- * - ListValue[?Const]
- * - ObjectValue[?Const].
- *
- * BooleanValue : one of `true` `false`
- *
- * NullValue : `null`
- *
- * EnumValue : Name but not `true`, `false` or `null`
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode
- */
- private function parseValueLiteral(bool $isConst): ValueNode
- {
- $this->increaseRecursionDepth();
-
- try {
- $token = $this->lexer->token;
- switch ($token->kind) {
- case Token::BRACKET_L:
- return $this->parseArray($isConst);
-
- case Token::BRACE_L:
- return $this->parseObject($isConst);
-
- case Token::INT:
- $this->lexer->advance();
-
- return new IntValueNode([
- 'value' => $token->value,
- 'loc' => $this->loc($token),
- ]);
-
- case Token::FLOAT:
- $this->lexer->advance();
-
- return new FloatValueNode([
- 'value' => $token->value,
- 'loc' => $this->loc($token),
- ]);
-
- case Token::STRING:
- case Token::BLOCK_STRING:
- return $this->parseStringLiteral();
-
- case Token::NAME:
- if ($token->value === 'true' || $token->value === 'false') {
- $this->lexer->advance();
-
- return new BooleanValueNode([
- 'value' => $token->value === 'true',
- 'loc' => $this->loc($token),
- ]);
- }
-
- if ($token->value === 'null') {
- $this->lexer->advance();
-
- return new NullValueNode([
- 'loc' => $this->loc($token),
- ]);
- }
- $this->lexer->advance();
-
- return new EnumValueNode([
- 'value' => $token->value,
- 'loc' => $this->loc($token),
- ]);
-
- case Token::DOLLAR:
- if (! $isConst) {
- return $this->parseVariable();
- }
-
- break;
- }
-
- throw $this->unexpected();
- } finally {
- --$this->recursionDepth;
- }
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseStringLiteral(): StringValueNode
- {
- $token = $this->lexer->token;
- $this->lexer->advance();
-
- return new StringValueNode([
- 'value' => $token->value,
- 'block' => $token->kind === Token::BLOCK_STRING,
- 'loc' => $this->loc($token),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseConstValue(): ValueNode
- {
- return $this->parseValueLiteral(true);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseVariableValue(): ValueNode
- {
- return $this->parseValueLiteral(false);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseArray(bool $isConst): ListValueNode
- {
- $start = $this->lexer->token;
- $parseFn = $isConst
- ? fn (): ValueNode => $this->parseConstValue()
- : fn (): ValueNode => $this->parseVariableValue();
-
- return new ListValueNode([
- 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseObject(bool $isConst): ObjectValueNode
- {
- $start = $this->lexer->token;
- $this->expect(Token::BRACE_L);
- $fields = [];
- while (! $this->skip(Token::BRACE_R)) {
- $fields[] = $this->parseObjectField($isConst);
- }
-
- return new ObjectValueNode([
- 'fields' => new NodeList($fields),
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseObjectField(bool $isConst): ObjectFieldNode
- {
- $start = $this->lexer->token;
- $name = $this->parseName();
-
- $this->expect(Token::COLON);
-
- return new ObjectFieldNode([
- 'name' => $name,
- 'value' => $this->parseValueLiteral($isConst),
- 'loc' => $this->loc($start),
- ]);
- }
-
- // Implements the parsing rules in the Directives section.
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<DirectiveNode>
- */
- private function parseDirectives(bool $isConst): NodeList
- {
- $directives = [];
- while ($this->peek(Token::AT)) {
- $directives[] = $this->parseDirective($isConst);
- }
-
- return new NodeList($directives);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseDirective(bool $isConst): DirectiveNode
- {
- $start = $this->lexer->token;
- $this->expect(Token::AT);
-
- return new DirectiveNode([
- 'name' => $this->parseName(),
- 'arguments' => $this->parseArguments($isConst),
- 'loc' => $this->loc($start),
- ]);
- }
-
- // Implements the parsing rules in the Types section.
-
- /**
- * Handles the Type: TypeName, ListType, and NonNullType parsing rules.
- *
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return ListTypeNode|NamedTypeNode|NonNullTypeNode
- */
- private function parseTypeReference(): TypeNode
- {
- $this->increaseRecursionDepth();
-
- try {
- $start = $this->lexer->token;
-
- if ($this->skip(Token::BRACKET_L)) {
- $type = $this->parseTypeReference();
- $this->expect(Token::BRACKET_R);
- $type = new ListTypeNode([
- 'type' => $type,
- 'loc' => $this->loc($start),
- ]);
- } else {
- $type = $this->parseNamedType();
- }
-
- if ($this->skip(Token::BANG)) {
- return new NonNullTypeNode([
- 'type' => $type,
- 'loc' => $this->loc($start),
- ]);
- }
-
- return $type;
- } finally {
- --$this->recursionDepth;
- }
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseNamedType(): NamedTypeNode
- {
- $start = $this->lexer->token;
-
- return new NamedTypeNode([
- 'name' => $this->parseName(),
- 'loc' => $this->loc($start),
- ]);
- }
-
- // Implements the parsing rules in the Type Definition section.
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return TypeSystemDefinitionNode&Node
- */
- private function parseTypeSystemDefinition(): TypeSystemDefinitionNode
- {
- // Many definitions begin with a description and require a lookahead.
- $keywordToken = $this->peekDescription()
- ? $this->lexer->lookahead()
- : $this->lexer->token;
-
- if ($keywordToken->kind === Token::NAME) {
- switch ($keywordToken->value) {
- case 'schema':
- return $this->parseSchemaDefinition();
-
- case 'scalar':
- return $this->parseScalarTypeDefinition();
-
- case 'type':
- return $this->parseObjectTypeDefinition();
-
- case 'interface':
- return $this->parseInterfaceTypeDefinition();
-
- case 'union':
- return $this->parseUnionTypeDefinition();
-
- case 'enum':
- return $this->parseEnumTypeDefinition();
-
- case 'input':
- return $this->parseInputObjectTypeDefinition();
-
- case 'directive':
- return $this->parseDirectiveDefinition();
- }
- }
-
- throw $this->unexpected($keywordToken);
- }
-
- private function peekDescription(): bool
- {
- return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseDescription(): ?StringValueNode
- {
- if ($this->peekDescription()) {
- return $this->parseStringLiteral();
- }
-
- return null;
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseSchemaDefinition(): SchemaDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('schema');
- $directives = $this->parseDirectives(true);
-
- $operationTypes = $this->many(
- Token::BRACE_L,
- fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(),
- Token::BRACE_R
- );
-
- return new SchemaDefinitionNode([
- 'directives' => $directives,
- 'operationTypes' => $operationTypes,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseOperationTypeDefinition(): OperationTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $operation = $this->parseOperationType();
- $this->expect(Token::COLON);
- $type = $this->parseNamedType();
-
- return new OperationTypeDefinitionNode([
- 'operation' => $operation,
- 'type' => $type,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseScalarTypeDefinition(): ScalarTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('scalar');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
-
- return new ScalarTypeDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseObjectTypeDefinition(): ObjectTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('type');
- $name = $this->parseName();
- $interfaces = $this->parseImplementsInterfaces();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseFieldsDefinition();
-
- return new ObjectTypeDefinitionNode([
- 'name' => $name,
- 'interfaces' => $interfaces,
- 'directives' => $directives,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<NamedTypeNode>
- */
- private function parseImplementsInterfaces(): NodeList
- {
- $types = [];
- if ($this->expectOptionalKeyword('implements')) {
- // Optional leading ampersand
- $this->skip(Token::AMP);
- do {
- $types[] = $this->parseNamedType();
- } while (
- $this->skip(Token::AMP)
- // Legacy support for the SDL?
- || (($this->lexer->options['allowLegacySDLImplementsInterfaces'] ?? false) && $this->peek(Token::NAME))
- );
- }
-
- return new NodeList($types);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<FieldDefinitionNode>
- */
- private function parseFieldsDefinition(): NodeList
- {
- // Legacy support for the SDL?
- if (
- ($this->lexer->options['allowLegacySDLEmptyFields'] ?? false)
- && $this->peek(Token::BRACE_L)
- && $this->lexer->lookahead()->kind === Token::BRACE_R
- ) {
- $this->lexer->advance();
- $this->lexer->advance();
-
- /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */
- $nodeList = new NodeList([]);
- } else {
- /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */
- $nodeList = $this->peek(Token::BRACE_L)
- ? $this->many(
- Token::BRACE_L,
- fn (): FieldDefinitionNode => $this->parseFieldDefinition(),
- Token::BRACE_R
- )
- : new NodeList([]);
- }
-
- return $nodeList;
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseFieldDefinition(): FieldDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $name = $this->parseName();
- $args = $this->parseArgumentsDefinition();
- $this->expect(Token::COLON);
- $type = $this->parseTypeReference();
- $directives = $this->parseDirectives(true);
-
- return new FieldDefinitionNode([
- 'name' => $name,
- 'arguments' => $args,
- 'type' => $type,
- 'directives' => $directives,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<InputValueDefinitionNode>
- */
- private function parseArgumentsDefinition(): NodeList
- {
- return $this->peek(Token::PAREN_L)
- ? $this->many(
- Token::PAREN_L,
- fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(),
- Token::PAREN_R
- )
- : new NodeList([]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseInputValueDefinition(): InputValueDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $name = $this->parseName();
- $this->expect(Token::COLON);
- $type = $this->parseTypeReference();
- $defaultValue = null;
- if ($this->skip(Token::EQUALS)) {
- $defaultValue = $this->parseConstValue();
- }
-
- $directives = $this->parseDirectives(true);
-
- return new InputValueDefinitionNode([
- 'name' => $name,
- 'type' => $type,
- 'defaultValue' => $defaultValue,
- 'directives' => $directives,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('interface');
- $name = $this->parseName();
- $interfaces = $this->parseImplementsInterfaces();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseFieldsDefinition();
-
- return new InterfaceTypeDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'interfaces' => $interfaces,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * UnionTypeDefinition :
- * - Description? union Name Directives[Const]? UnionMemberTypes?
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseUnionTypeDefinition(): UnionTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('union');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $types = $this->parseUnionMemberTypes();
-
- return new UnionTypeDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'types' => $types,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<NamedTypeNode>
- */
- private function parseUnionMemberTypes(): NodeList
- {
- $types = [];
- if ($this->skip(Token::EQUALS)) {
- // Optional leading pipe
- $this->skip(Token::PIPE);
- do {
- $types[] = $this->parseNamedType();
- } while ($this->skip(Token::PIPE));
- }
-
- return new NodeList($types);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseEnumTypeDefinition(): EnumTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('enum');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $values = $this->parseEnumValuesDefinition();
-
- return new EnumTypeDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'values' => $values,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<EnumValueDefinitionNode>
- */
- private function parseEnumValuesDefinition(): NodeList
- {
- return $this->peek(Token::BRACE_L)
- ? $this->many(
- Token::BRACE_L,
- fn (): EnumValueDefinitionNode => $this->parseEnumValueDefinition(),
- Token::BRACE_R
- )
- : new NodeList([]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseEnumValueDefinition(): EnumValueDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
-
- return new EnumValueDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('input');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseInputFieldsDefinition();
-
- return new InputObjectTypeDefinitionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- 'description' => $description,
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<InputValueDefinitionNode>
- */
- private function parseInputFieldsDefinition(): NodeList
- {
- return $this->peek(Token::BRACE_L)
- ? $this->many(
- Token::BRACE_L,
- fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(),
- Token::BRACE_R
- )
- : new NodeList([]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return TypeSystemExtensionNode&Node
- */
- private function parseTypeSystemExtension(): TypeSystemExtensionNode
- {
- $keywordToken = $this->lexer->lookahead();
-
- if ($keywordToken->kind === Token::NAME) {
- switch ($keywordToken->value) {
- case 'schema':
- return $this->parseSchemaTypeExtension();
-
- case 'scalar':
- return $this->parseScalarTypeExtension();
-
- case 'type':
- return $this->parseObjectTypeExtension();
-
- case 'interface':
- return $this->parseInterfaceTypeExtension();
-
- case 'union':
- return $this->parseUnionTypeExtension();
-
- case 'enum':
- return $this->parseEnumTypeExtension();
-
- case 'input':
- return $this->parseInputObjectTypeExtension();
- }
- }
-
- throw $this->unexpected($keywordToken);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseSchemaTypeExtension(): SchemaExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('schema');
- $directives = $this->parseDirectives(true);
-
- $operationTypes = $this->peek(Token::BRACE_L)
- ? $this->many(
- Token::BRACE_L,
- fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(),
- Token::BRACE_R
- )
- : new NodeList([]);
- if (count($directives) === 0 && count($operationTypes) === 0) {
- $this->unexpected();
- }
-
- return new SchemaExtensionNode([
- 'directives' => $directives,
- 'operationTypes' => $operationTypes,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseScalarTypeExtension(): ScalarTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('scalar');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- if (count($directives) === 0) {
- throw $this->unexpected();
- }
-
- return new ScalarTypeExtensionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseObjectTypeExtension(): ObjectTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('type');
- $name = $this->parseName();
- $interfaces = $this->parseImplementsInterfaces();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseFieldsDefinition();
-
- if (
- count($interfaces) === 0
- && count($directives) === 0
- && count($fields) === 0
- ) {
- throw $this->unexpected();
- }
-
- return new ObjectTypeExtensionNode([
- 'name' => $name,
- 'interfaces' => $interfaces,
- 'directives' => $directives,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseInterfaceTypeExtension(): InterfaceTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('interface');
- $name = $this->parseName();
- $interfaces = $this->parseImplementsInterfaces();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseFieldsDefinition();
- if (
- count($interfaces) === 0
- && count($directives) === 0
- && count($fields) === 0
- ) {
- throw $this->unexpected();
- }
-
- return new InterfaceTypeExtensionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'interfaces' => $interfaces,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * UnionTypeExtension :
- * - extend union Name Directives[Const]? UnionMemberTypes
- * - extend union Name Directives[Const].
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseUnionTypeExtension(): UnionTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('union');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $types = $this->parseUnionMemberTypes();
- if (count($directives) === 0 && count($types) === 0) {
- throw $this->unexpected();
- }
-
- return new UnionTypeExtensionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'types' => $types,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseEnumTypeExtension(): EnumTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('enum');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $values = $this->parseEnumValuesDefinition();
- if (
- count($directives) === 0
- && count($values) === 0
- ) {
- throw $this->unexpected();
- }
-
- return new EnumTypeExtensionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'values' => $values,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseInputObjectTypeExtension(): InputObjectTypeExtensionNode
- {
- $start = $this->lexer->token;
- $this->expectKeyword('extend');
- $this->expectKeyword('input');
- $name = $this->parseName();
- $directives = $this->parseDirectives(true);
- $fields = $this->parseInputFieldsDefinition();
- if (
- count($directives) === 0
- && count($fields) === 0
- ) {
- throw $this->unexpected();
- }
-
- return new InputObjectTypeExtensionNode([
- 'name' => $name,
- 'directives' => $directives,
- 'fields' => $fields,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * DirectiveDefinition :
- * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations.
- *
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseDirectiveDefinition(): DirectiveDefinitionNode
- {
- $start = $this->lexer->token;
- $description = $this->parseDescription();
- $this->expectKeyword('directive');
- $this->expect(Token::AT);
- $name = $this->parseName();
- $args = $this->parseArgumentsDefinition();
- $repeatable = $this->expectOptionalKeyword('repeatable');
- $this->expectKeyword('on');
- $locations = $this->parseDirectiveLocations();
-
- return new DirectiveDefinitionNode([
- 'name' => $name,
- 'description' => $description,
- 'arguments' => $args,
- 'repeatable' => $repeatable,
- 'locations' => $locations,
- 'loc' => $this->loc($start),
- ]);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- *
- * @return NodeList<NameNode>
- */
- private function parseDirectiveLocations(): NodeList
- {
- // Optional leading pipe
- $this->skip(Token::PIPE);
- $locations = [];
- do {
- $locations[] = $this->parseDirectiveLocation();
- } while ($this->skip(Token::PIPE));
-
- return new NodeList($locations);
- }
-
- /**
- * @throws \JsonException
- * @throws SyntaxError
- */
- private function parseDirectiveLocation(): NameNode
- {
- $start = $this->lexer->token;
- $name = $this->parseName();
- if (DirectiveLocation::has($name->value)) {
- return $name;
- }
-
- throw $this->unexpected($start);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php
deleted file mode 100644
index fe1b912a173..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php
+++ /dev/null
@@ -1,525 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-
-/**
- * Prints AST to string. Capable of printing Automattic\WooCommerce\Vendor\GraphQL queries and Type definition language.
- * Useful for pretty-printing queries or printing back AST for logging, documentation, etc.
- *
- * Usage example:
- *
- * ```php
- * $query = 'query myQuery {someField}';
- * $ast = Automattic\WooCommerce\Vendor\GraphQL\Language\Parser::parse($query);
- * $printed = Automattic\WooCommerce\Vendor\GraphQL\Language\Printer::doPrint($ast);
- * ```
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\PrinterTest
- */
-class Printer
-{
- /**
- * Converts the AST of a Automattic\WooCommerce\Vendor\GraphQL node to a string.
- *
- * Handles both executable definitions and schema definitions.
- *
- * @throws \JsonException
- *
- * @api
- */
- public static function doPrint(Node $ast): string
- {
- return static::p($ast);
- }
-
- /** @throws \JsonException */
- protected static function p(?Node $node): string
- {
- if ($node === null) {
- return '';
- }
-
- switch (true) {
- case $node instanceof ArgumentNode:
- case $node instanceof ObjectFieldNode:
- return static::p($node->name) . ': ' . static::p($node->value);
-
- case $node instanceof BooleanValueNode:
- return $node->value
- ? 'true'
- : 'false';
-
- case $node instanceof DirectiveDefinitionNode:
- $argStrings = [];
- foreach ($node->arguments as $arg) {
- $argStrings[] = static::p($arg);
- }
-
- $noIndent = true;
- foreach ($argStrings as $argString) {
- if (strpos($argString, "\n") !== false) {
- $noIndent = false;
- break;
- }
- }
-
- return static::addDescription($node->description, 'directive @'
- . static::p($node->name)
- . ($noIndent
- ? static::wrap('(', static::join($argStrings, ', '), ')')
- : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n"))
- . ($node->repeatable
- ? ' repeatable'
- : '')
- . ' on ' . static::printList($node->locations, ' | '));
-
- case $node instanceof DirectiveNode:
- return '@' . static::p($node->name) . static::wrap('(', static::printList($node->arguments, ', '), ')');
-
- case $node instanceof DocumentNode:
- return static::printList($node->definitions, "\n\n") . "\n";
-
- case $node instanceof EnumTypeDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- 'enum',
- static::p($node->name),
- static::printList($node->directives, ' '),
- static::printListBlock($node->values),
- ],
- ' '
- ));
-
- case $node instanceof EnumTypeExtensionNode:
- return static::join(
- [
- 'extend enum',
- static::p($node->name),
- static::printList($node->directives, ' '),
- static::printListBlock($node->values),
- ],
- ' '
- );
-
- case $node instanceof EnumValueDefinitionNode:
- return static::addDescription(
- $node->description,
- static::join([static::p($node->name), static::printList($node->directives, ' ')], ' ')
- );
-
- case $node instanceof EnumValueNode:
- case $node instanceof FloatValueNode:
- case $node instanceof IntValueNode:
- case $node instanceof NameNode:
- return $node->value;
-
- case $node instanceof FieldDefinitionNode:
- $argStrings = [];
- foreach ($node->arguments as $item) {
- $argStrings[] = static::p($item);
- }
-
- $noIndent = true;
- foreach ($argStrings as $argString) {
- if (strpos($argString, "\n") !== false) {
- $noIndent = false;
- break;
- }
- }
-
- return static::addDescription(
- $node->description,
- static::p($node->name)
- . ($noIndent
- ? static::wrap('(', static::join($argStrings, ', '), ')')
- : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n)"))
- . ': ' . static::p($node->type)
- . static::wrap(' ', static::printList($node->directives, ' '))
- );
-
- case $node instanceof FieldNode:
- $prefix = static::wrap('', $node->alias->value ?? null, ': ') . static::p($node->name);
-
- $argsLine = $prefix . static::wrap(
- '(',
- static::printList($node->arguments, ', '),
- ')'
- );
- if (strlen($argsLine) > 80) {
- $argsLine = $prefix . static::wrap(
- "(\n",
- static::indent(
- static::printList($node->arguments, "\n")
- ),
- "\n)"
- );
- }
-
- return static::join(
- [
- $argsLine,
- static::printList($node->directives, ' '),
- static::p($node->selectionSet),
- ],
- ' '
- );
-
- case $node instanceof FragmentDefinitionNode:
- // Note: fragment variable definitions are experimental and may be changed or removed in the future.
- return 'fragment ' . static::p($node->name)
- . static::wrap(
- '(',
- static::printList($node->variableDefinitions ?? new NodeList([]), ', '),
- ')'
- )
- . ' on ' . static::p($node->typeCondition->name) . ' '
- . static::wrap(
- '',
- static::printList($node->directives, ' '),
- ' '
- )
- . static::p($node->selectionSet);
-
- case $node instanceof FragmentSpreadNode:
- return '...'
- . static::p($node->name)
- . static::wrap(' ', static::printList($node->directives, ' '));
-
- case $node instanceof InlineFragmentNode:
- return static::join(
- [
- '...',
- static::wrap('on ', static::p($node->typeCondition->name ?? null)),
- static::printList($node->directives, ' '),
- static::p($node->selectionSet),
- ],
- ' '
- );
-
- case $node instanceof InputObjectTypeDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- 'input',
- static::p($node->name),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- ));
-
- case $node instanceof InputObjectTypeExtensionNode:
- return static::join(
- [
- 'extend input',
- static::p($node->name),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- );
-
- case $node instanceof InputValueDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- static::p($node->name) . ': ' . static::p($node->type),
- static::wrap('= ', static::p($node->defaultValue)),
- static::printList($node->directives, ' '),
- ],
- ' '
- ));
-
- case $node instanceof InterfaceTypeDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- 'interface',
- static::p($node->name),
- static::wrap('implements ', static::printList($node->interfaces, ' & ')),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- ));
-
- case $node instanceof InterfaceTypeExtensionNode:
- return static::join(
- [
- 'extend interface',
- static::p($node->name),
- static::wrap('implements ', static::printList($node->interfaces, ' & ')),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- );
-
- case $node instanceof ListTypeNode:
- return '[' . static::p($node->type) . ']';
-
- case $node instanceof ListValueNode:
- return '[' . static::printList($node->values, ', ') . ']';
-
- case $node instanceof NamedTypeNode:
- return static::p($node->name);
-
- case $node instanceof NonNullTypeNode:
- return static::p($node->type) . '!';
-
- case $node instanceof NullValueNode:
- return 'null';
-
- case $node instanceof ObjectTypeDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- 'type',
- static::p($node->name),
- static::wrap('implements ', static::printList($node->interfaces, ' & ')),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- ));
-
- case $node instanceof ObjectTypeExtensionNode:
- return static::join(
- [
- 'extend type',
- static::p($node->name),
- static::wrap('implements ', static::printList($node->interfaces, ' & ')),
- static::printList($node->directives, ' '),
- static::printListBlock($node->fields),
- ],
- ' '
- );
-
- case $node instanceof ObjectValueNode:
- return '{ '
- . static::printList($node->fields, ', ')
- . ' }';
-
- case $node instanceof OperationDefinitionNode:
- $op = $node->operation;
- $name = static::p($node->name);
- $varDefs = static::wrap('(', static::printList($node->variableDefinitions, ', '), ')');
- $directives = static::printList($node->directives, ' ');
- $selectionSet = static::p($node->selectionSet);
-
- // Anonymous queries with no directives or variable definitions can use
- // the query short form.
- return $name === '' && $directives === '' && $varDefs === '' && $op === 'query'
- ? $selectionSet
- : static::join([$op, static::join([$name, $varDefs]), $directives, $selectionSet], ' ');
-
- case $node instanceof OperationTypeDefinitionNode:
- return $node->operation . ': ' . static::p($node->type);
-
- case $node instanceof ScalarTypeDefinitionNode:
- return static::addDescription($node->description, static::join([
- 'scalar',
- static::p($node->name),
- static::printList($node->directives, ' '),
- ], ' '));
-
- case $node instanceof ScalarTypeExtensionNode:
- return static::join(
- [
- 'extend scalar',
- static::p($node->name),
- static::printList($node->directives, ' '),
- ],
- ' '
- );
-
- case $node instanceof SchemaDefinitionNode:
- return static::addDescription($node->description, static::join(
- [
- 'schema',
- static::printList($node->directives, ' '),
- static::printListBlock($node->operationTypes),
- ],
- ' '
- ));
-
- case $node instanceof SchemaExtensionNode:
- return static::join(
- [
- 'extend schema',
- static::printList($node->directives, ' '),
- static::printListBlock($node->operationTypes),
- ],
- ' '
- );
-
- case $node instanceof SelectionSetNode:
- return static::printListBlock($node->selections);
-
- case $node instanceof StringValueNode:
- if ($node->block) {
- return BlockString::print($node->value);
- }
-
- return json_encode($node->value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
-
- case $node instanceof UnionTypeDefinitionNode:
- $typesStr = static::printList($node->types, ' | ');
-
- return static::addDescription($node->description, static::join(
- [
- 'union',
- static::p($node->name),
- static::printList($node->directives, ' '),
- $typesStr !== ''
- ? "= {$typesStr}"
- : '',
- ],
- ' '
- ));
-
- case $node instanceof UnionTypeExtensionNode:
- $typesStr = static::printList($node->types, ' | ');
-
- return static::join(
- [
- 'extend union',
- static::p($node->name),
- static::printList($node->directives, ' '),
- $typesStr !== ''
- ? "= {$typesStr}"
- : '',
- ],
- ' '
- );
-
- case $node instanceof VariableDefinitionNode:
- return '$' . static::p($node->variable->name)
- . ': '
- . static::p($node->type)
- . static::wrap(' = ', static::p($node->defaultValue))
- . static::wrap(' ', static::printList($node->directives, ' '));
-
- case $node instanceof VariableNode:
- return '$' . static::p($node->name);
- }
-
- return '';
- }
-
- /**
- * @template TNode of Node
- *
- * @param NodeList<TNode> $list
- *
- * @throws \JsonException
- */
- protected static function printList(NodeList $list, string $separator = ''): string
- {
- $parts = [];
- foreach ($list as $item) {
- $parts[] = static::p($item);
- }
-
- return static::join($parts, $separator);
- }
-
- /**
- * Print each item on its own line, wrapped in an indented "{ }" block.
- *
- * @template TNode of Node
- *
- * @param NodeList<TNode> $list
- *
- * @throws \JsonException
- */
- protected static function printListBlock(NodeList $list): string
- {
- if (count($list) === 0) {
- return '';
- }
-
- $parts = [];
- foreach ($list as $item) {
- $parts[] = static::p($item);
- }
-
- return "{\n" . static::indent(static::join($parts, "\n")) . "\n}";
- }
-
- /** @throws \JsonException */
- protected static function addDescription(?StringValueNode $description, string $body): string
- {
- return static::join([static::p($description), $body], "\n");
- }
-
- /**
- * If maybeString is not null or empty, then wrap with start and end, otherwise
- * print an empty string.
- */
- protected static function wrap(string $start, ?string $maybeString, string $end = ''): string
- {
- if ($maybeString === null || $maybeString === '') {
- return '';
- }
-
- return $start . $maybeString . $end;
- }
-
- protected static function indent(string $string): string
- {
- if ($string === '') {
- return '';
- }
-
- return ' ' . str_replace("\n", "\n ", $string);
- }
-
- /** @param array<string|null> $parts */
- protected static function join(array $parts, string $separator = ''): string
- {
- return implode($separator, array_filter($parts, static fn (?string $part) => $part !== '' && $part !== null));
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php
deleted file mode 100644
index fd9a662090d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-class Source
-{
- public string $body;
-
- public int $length;
-
- public string $name;
-
- public SourceLocation $locationOffset;
-
- /**
- * A representation of source input to GraphQL.
- *
- * `name` and `locationOffset` are optional. They are useful for clients who
- * store Automattic\WooCommerce\Vendor\GraphQL documents in source files; for example, if the Automattic\WooCommerce\Vendor\GraphQL input
- * starts at line 40 in a file named Foo.graphql, it might be useful for name to
- * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
- * line and column in locationOffset are 1-indexed
- */
- public function __construct(string $body, ?string $name = null, ?SourceLocation $location = null)
- {
- $this->body = $body;
- $this->length = mb_strlen($body, 'UTF-8');
- $this->name = $name === '' || $name === null
- ? 'Automattic\WooCommerce\Vendor\GraphQL request'
- : $name;
- $this->locationOffset = $location ?? new SourceLocation(1, 1);
- }
-
- public function getLocation(int $position): SourceLocation
- {
- $line = 1;
- $column = $position + 1;
-
- $utfChars = json_decode('"\u2028\u2029"');
- $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su';
- $matches = [];
- preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, \PREG_OFFSET_CAPTURE);
-
- foreach ($matches[0] as $match) {
- ++$line;
-
- $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
- }
-
- return new SourceLocation($line, $column);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php
deleted file mode 100644
index 40bc74dc762..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-class SourceLocation implements \JsonSerializable
-{
- public int $line;
-
- public int $column;
-
- public function __construct(int $line, int $col)
- {
- $this->line = $line;
- $this->column = $col;
- }
-
- /** @return array{line: int, column: int} */
- public function toArray(): array
- {
- return [
- 'line' => $this->line,
- 'column' => $this->column,
- ];
- }
-
- /** @return array{line: int, column: int} */
- public function toSerializableArray(): array
- {
- return $this->toArray();
- }
-
- /** @return array{line: int, column: int} */
- #[\ReturnTypeWillChange]
- public function jsonSerialize(): array
- {
- return $this->toArray();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php
deleted file mode 100644
index 1f6edb0ecac..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-/**
- * Represents a range of characters represented by a lexical token
- * within a Source.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\TokenTest
- */
-class Token
-{
- // Each kind of token.
- public const SOF = '<SOF>';
- public const EOF = '<EOF>';
- public const BANG = '!';
- public const DOLLAR = '$';
- public const AMP = '&';
- public const PAREN_L = '(';
- public const PAREN_R = ')';
- public const SPREAD = '...';
- public const COLON = ':';
- public const EQUALS = '=';
- public const AT = '@';
- public const BRACKET_L = '[';
- public const BRACKET_R = ']';
- public const BRACE_L = '{';
- public const PIPE = '|';
- public const BRACE_R = '}';
- public const NAME = 'Name';
- public const INT = 'Int';
- public const FLOAT = 'Float';
- public const STRING = 'String';
- public const BLOCK_STRING = 'BlockString';
- public const COMMENT = 'Comment';
-
- /** The kind of Token (see one of constants above). */
- public string $kind;
-
- /** The character offset at which this Node begins. */
- public int $start;
-
- /** The character offset at which this Node ends. */
- public int $end;
-
- /** The 1-indexed line number on which this Token appears. */
- public int $line;
-
- /** The 1-indexed column number at which this Token begins. */
- public int $column;
-
- public ?string $value;
-
- /**
- * Tokens exist as nodes in a double-linked-list amongst all tokens
- * including ignored tokens. <SOF> is always the first node and <EOF>
- * the last.
- */
- public ?Token $prev;
-
- public ?Token $next = null;
-
- public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, ?string $value = null)
- {
- $this->kind = $kind;
- $this->start = $start;
- $this->end = $end;
- $this->line = $line;
- $this->column = $column;
- $this->prev = $previous;
- $this->value = $value;
- }
-
- public function getDescription(): string
- {
- return $this->kind
- . ($this->value === null
- ? ''
- : " \"{$this->value}\"");
- }
-
- /**
- * @return array{
- * kind: string,
- * value: string|null,
- * line: int,
- * column: int,
- * }
- */
- public function toArray(): array
- {
- return [
- 'kind' => $this->kind,
- 'value' => $this->value,
- 'line' => $this->line,
- 'column' => $this->column,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php
deleted file mode 100644
index 2dbf722b5e5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php
+++ /dev/null
@@ -1,526 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Utility for efficient AST traversal and modification.
- *
- * `visit()` will walk through an AST using a depth first traversal, calling
- * the visitor's enter function at each node in the traversal, and calling the
- * leave function after visiting that node and all of its child nodes.
- *
- * By returning different values from the `enter` and `leave` functions, the behavior of the visitor can be altered.
- *
- * - no return (`void`) or return `null`: no action
- * - `Visitor::skipNode()`: skips over the subtree at the current node of the AST
- * - `Visitor::stop()`: stop the Visitor completely
- * - `Visitor::removeNode()`: remove the current node
- * - return any other value: replace this node with the returned value
- *
- * When using `visit()` to edit an AST, the original AST will not be modified, and
- * a new version of the AST with the changes applied will be returned from the
- * visit function.
- *
- * ```php
- * $editedAST = Visitor::visit($ast, [
- * 'enter' => function (Node $node, $key, $parent, array $path, array $ancestors) {
- * // ...
- * },
- * 'leave' => function (Node $node, $key, $parent, array $path, array $ancestors) {
- * // ...
- * }
- * ]);
- * ```
- *
- * Alternatively to providing `enter` and `leave` functions, a visitor can
- * instead provide functions named the same as the [kinds of AST nodes](class-reference.md#graphqllanguageastnodekind),
- * or enter/leave visitors at a named key, leading to four permutations of
- * visitor API:
- *
- * 1. Named visitors triggered when entering a node a specific kind.
- *
- * ```php
- * Visitor::visit($ast, [
- * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
- * // enter the "ObjectTypeDefinition" node
- * }
- * ]);
- * ```
- *
- * 2. Named visitors that trigger upon entering and leaving a node of
- * a specific kind.
- *
- * ```php
- * Visitor::visit($ast, [
- * NodeKind::OBJECT_TYPE_DEFINITION => [
- * 'enter' => function (ObjectTypeDefinitionNode $node) {
- * // enter the "ObjectTypeDefinition" node
- * },
- * 'leave' => function (ObjectTypeDefinitionNode $node) {
- * // leave the "ObjectTypeDefinition" node
- * }
- * ]
- * ]);
- * ```
- *
- * 3. Generic visitors that trigger upon entering and leaving any node.
- *
- * ```php
- * Visitor::visit($ast, [
- * 'enter' => function (Node $node) {
- * // enter any node
- * },
- * 'leave' => function (Node $node) {
- * // leave any node
- * }
- * ]);
- * ```
- *
- * 4. Parallel visitors for entering and leaving nodes of a specific kind.
- *
- * ```php
- * Visitor::visit($ast, [
- * 'enter' => [
- * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
- * // enter the "ObjectTypeDefinition" node
- * }
- * ],
- * 'leave' => [
- * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
- * // leave the "ObjectTypeDefinition" node
- * }
- * ]
- * ]);
- * ```
- *
- * @phpstan-type NodeVisitor callable(Node): (VisitorOperation|Node|NodeList<Node>|null|false|void)
- * @phpstan-type VisitorArray array<string, NodeVisitor>|array<string, array<string, NodeVisitor>>
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\VisitorTest
- */
-class Visitor
-{
- public const VISITOR_KEYS = [
- NodeKind::NAME => [],
- NodeKind::DOCUMENT => ['definitions'],
- NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
- NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue', 'directives'],
- NodeKind::VARIABLE => ['name'],
- NodeKind::SELECTION_SET => ['selections'],
- NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
- NodeKind::ARGUMENT => ['name', 'value'],
- NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
- NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
- NodeKind::FRAGMENT_DEFINITION => [
- 'name',
- // Note: fragment variable definitions are experimental and may be changed
- // or removed in the future.
- 'variableDefinitions',
- 'typeCondition',
- 'directives',
- 'selectionSet',
- ],
-
- NodeKind::INT => [],
- NodeKind::FLOAT => [],
- NodeKind::STRING => [],
- NodeKind::BOOLEAN => [],
- NodeKind::NULL => [],
- NodeKind::ENUM => [],
- NodeKind::LST => ['values'],
- NodeKind::OBJECT => ['fields'],
- NodeKind::OBJECT_FIELD => ['name', 'value'],
- NodeKind::DIRECTIVE => ['name', 'arguments'],
- NodeKind::NAMED_TYPE => ['name'],
- NodeKind::LIST_TYPE => ['type'],
- NodeKind::NON_NULL_TYPE => ['type'],
-
- NodeKind::SCHEMA_DEFINITION => ['description', 'directives', 'operationTypes'],
- NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
- NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
- NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
- NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
- NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
- NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
- NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
- NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
- NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
- NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
-
- NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
- NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
- NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
- NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
- NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
- NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
-
- NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
-
- NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'],
- ];
-
- /**
- * Visit the AST (see class description for details).
- *
- * @param NodeList<Node>|Node $root
- * @param VisitorArray $visitor
- * @param array<string, mixed>|null $keyMap
- *
- * @throws \Exception
- *
- * @return mixed
- *
- * @api
- */
- public static function visit(object $root, array $visitor, ?array $keyMap = null)
- {
- $visitorKeys = $keyMap ?? self::VISITOR_KEYS;
-
- /**
- * @var list<array{
- * inList: bool,
- * index: int,
- * keys: Node|NodeList|mixed,
- * edits: array<int, array{mixed, mixed}>,
- * }> $stack */
- $stack = [];
- $inList = $root instanceof NodeList;
- $keys = [$root];
- $index = -1;
- $edits = [];
- $parent = null;
- $path = [];
- $ancestors = [];
-
- do {
- ++$index;
- $isLeaving = $index === count($keys);
- $key = null;
- $node = null;
- $isEdited = $isLeaving && $edits !== [];
-
- if ($isLeaving) {
- $key = $ancestors === []
- ? null
- : $path[count($path) - 1];
- $node = $parent;
- $parent = array_pop($ancestors);
- if ($isEdited) {
- if ($node instanceof Node || $node instanceof NodeList) {
- $node = $node->cloneDeep();
- }
-
- $editOffset = 0;
- foreach ($edits as [$editKey, $editValue]) {
- if ($inList) {
- $editKey -= $editOffset;
- }
-
- if ($inList && $editValue === null) {
- assert($node instanceof NodeList, 'Follows from $inList');
- $node->splice($editKey, 1);
- ++$editOffset;
- } elseif ($node instanceof NodeList) {
- if ($editValue instanceof NodeList) {
- $node->splice($editKey, 1, $editValue);
- $editOffset -= count($editValue) - 1;
- } elseif ($editValue instanceof Node) {
- $node[$editKey] = $editValue;
- } else {
- $notNodeOrNodeList = Utils::printSafe($editValue);
- throw new \Exception("Can only add Node or NodeList to NodeList, got: {$notNodeOrNodeList}.");
- }
- } else {
- $node->{$editKey} = $editValue;
- }
- }
- }
- // @phpstan-ignore-next-line the stack is guaranteed to be non-empty at this point
- [
- 'index' => $index,
- 'keys' => $keys,
- 'edits' => $edits,
- 'inList' => $inList,
- ] = array_pop($stack);
- } elseif ($parent === null) {
- $node = $root;
- } else {
- $key = $inList
- ? $index
- : $keys[$index];
- $node = $parent instanceof NodeList
- ? $parent[$key]
- : $parent->{$key};
- if ($node === null) {
- continue;
- }
- $path[] = $key;
- }
-
- $result = null;
- if (! $node instanceof NodeList) {
- if (! $node instanceof Node) {
- $notNode = Utils::printSafe($node);
- throw new \Exception("Invalid AST Node: {$notNode}.");
- }
-
- $visitFn = self::extractVisitFn($visitor, $node->kind, $isLeaving);
-
- if ($visitFn !== null) {
- $result = $visitFn($node, $key, $parent, $path, $ancestors);
-
- if ($result !== null) {
- if ($result instanceof VisitorStop) {
- break;
- }
-
- if ($result instanceof VisitorSkipNode) {
- if (! $isLeaving) {
- array_pop($path);
- }
- continue;
- }
-
- $editValue = $result instanceof VisitorRemoveNode
- ? null
- : $result;
-
- $edits[] = [$key, $editValue];
- if (! $isLeaving) {
- if (! $editValue instanceof Node) {
- array_pop($path);
- continue;
- }
-
- $node = $editValue;
- }
- }
- }
- }
-
- if ($result === null && $isEdited) {
- $edits[] = [$key, $node];
- }
-
- if ($isLeaving) {
- array_pop($path);
- } else {
- $stack[] = [
- 'inList' => $inList,
- 'index' => $index,
- 'keys' => $keys,
- 'edits' => $edits,
- ];
- $inList = $node instanceof NodeList;
-
- $keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? [];
- $index = -1;
- $edits = [];
- if ($parent !== null) {
- $ancestors[] = $parent;
- }
-
- $parent = $node;
- }
- } while ($stack !== []);
-
- return $edits === []
- ? $root
- : $edits[0][1];
- }
-
- /**
- * Returns marker for stopping.
- *
- * @api
- */
- public static function stop(): VisitorStop
- {
- static $stop;
-
- return $stop ??= new VisitorStop();
- }
-
- /**
- * Returns marker for skipping the subtree at the current node.
- *
- * @api
- */
- public static function skipNode(): VisitorSkipNode
- {
- static $skipNode;
-
- return $skipNode ??= new VisitorSkipNode();
- }
-
- /**
- * Returns marker for removing the current node.
- *
- * @api
- */
- public static function removeNode(): VisitorRemoveNode
- {
- static $removeNode;
-
- return $removeNode ??= new VisitorRemoveNode();
- }
-
- /**
- * Combines the given visitors to run in parallel.
- *
- * @phpstan-param array<int, VisitorArray> $visitors
- *
- * @return VisitorArray
- */
- public static function visitInParallel(array $visitors): array
- {
- $visitorsCount = count($visitors);
- $skipping = new \SplFixedArray($visitorsCount);
-
- return [
- 'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
- for ($i = 0; $i < $visitorsCount; ++$i) {
- if ($skipping[$i] !== null) {
- continue;
- }
-
- $fn = self::extractVisitFn(
- $visitors[$i],
- $node->kind,
- false
- );
-
- if ($fn === null) {
- continue;
- }
-
- $result = $fn(...func_get_args());
-
- if ($result === null) {
- continue;
- }
- if ($result instanceof VisitorSkipNode) {
- $skipping[$i] = $node;
- } elseif ($result instanceof VisitorStop) {
- $skipping[$i] = $result;
- } else {
- return $result;
- }
- }
-
- return null;
- },
- 'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
- for ($i = 0; $i < $visitorsCount; ++$i) {
- if ($skipping[$i] === null) {
- $fn = self::extractVisitFn(
- $visitors[$i],
- $node->kind,
- true
- );
-
- if ($fn !== null) {
- $result = $fn(...func_get_args());
-
- if ($result === null) {
- continue;
- }
- if ($result instanceof VisitorStop) {
- $skipping[$i] = $result;
- } elseif ($result instanceof VisitorRemoveNode) {
- return $result;
- } else {
- return $result;
- }
- }
- } elseif ($skipping[$i] === $node) {
- $skipping[$i] = null;
- }
- }
-
- return null;
- },
- ];
- }
-
- /**
- * Creates a new visitor that updates TypeInfo and delegates to the given visitor.
- *
- * @phpstan-param VisitorArray $visitor
- *
- * @phpstan-return VisitorArray
- */
- public static function visitWithTypeInfo(TypeInfo $typeInfo, array $visitor): array
- {
- return [
- 'enter' => static function (Node $node) use ($typeInfo, $visitor) {
- $typeInfo->enter($node);
- $fn = self::extractVisitFn($visitor, $node->kind, false);
-
- if ($fn === null) {
- return null;
- }
-
- $result = $fn(...func_get_args());
- if ($result === null) {
- return null;
- }
-
- $typeInfo->leave($node);
- if ($result instanceof Node) {
- $typeInfo->enter($result);
- }
-
- return $result;
- },
- 'leave' => static function (Node $node) use ($typeInfo, $visitor) {
- $fn = self::extractVisitFn($visitor, $node->kind, true);
- $result = $fn !== null
- ? $fn(...func_get_args())
- : null;
-
- $typeInfo->leave($node);
-
- return $result;
- },
- ];
- }
-
- /**
- * @phpstan-param VisitorArray $visitor
- *
- * @return (callable(Node $node, string|int|null $key, Node|NodeList<Node>|null $parent, array<int, int|string> $path, array<int, Node|NodeList<Node>> $ancestors): (VisitorOperation|Node|null))|(callable(Node): (VisitorOperation|Node|NodeList<Node>|void|false|null))|null
- */
- protected static function extractVisitFn(array $visitor, string $kind, bool $isLeaving): ?callable
- {
- $kindVisitor = $visitor[$kind] ?? null;
-
- if ($kindVisitor !== null) {
- if (is_array($kindVisitor)) {
- return $isLeaving
- ? $kindVisitor['leave'] ?? null
- : $kindVisitor['enter'] ?? null;
- }
-
- if (! $isLeaving) {
- return $kindVisitor;
- }
- }
-
- $specificVisitor = $isLeaving
- ? $visitor['leave'] ?? null
- : $visitor['enter'] ?? null;
-
- if ($specificVisitor !== null && is_array($specificVisitor)) {
- return $specificVisitor[$kind] ?? null;
- }
-
- return $specificVisitor;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php
deleted file mode 100644
index 8bf68bae570..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-abstract class VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php
deleted file mode 100644
index cb129188eae..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-final class VisitorRemoveNode extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php
deleted file mode 100644
index 134cdd1d7c5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-final class VisitorSkipNode extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php
deleted file mode 100644
index 4457992ed6f..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
-
-final class VisitorStop extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php
deleted file mode 100644
index fd74f68f153..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class BatchedQueriesAreNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php
deleted file mode 100644
index 50726a3bcc0..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class CannotParseJsonBody extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php
deleted file mode 100644
index 98684060fdd..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class CannotParseVariables extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php
deleted file mode 100644
index c434a557b46..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class CannotReadBody extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php
deleted file mode 100644
index 135c144cb8e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class FailedToDetermineOperationType extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php
deleted file mode 100644
index 1143b988246..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class GetMethodSupportsOnlyQueryOperation extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php
deleted file mode 100644
index 72dec119415..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class HttpMethodNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php
deleted file mode 100644
index 5f31366704a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class InvalidOperationParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php
deleted file mode 100644
index 826a8807c6e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class InvalidQueryIdParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php
deleted file mode 100644
index ba3e9a8a1ae..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class InvalidQueryParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php
deleted file mode 100644
index 939a811b2cc..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class MissingContentTypeHeader extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php
deleted file mode 100644
index 68dea3fd3ea..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class MissingQueryOrQueryIdParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php
deleted file mode 100644
index eefc8aa7c1c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class PersistedQueriesAreNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php
deleted file mode 100644
index 934335c9485..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
-
-class UnexpectedContentType extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php
deleted file mode 100644
index c1eae7a18a5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php
+++ /dev/null
@@ -1,573 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\BatchedQueriesAreNotSupported;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotParseJsonBody;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotParseVariables;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotReadBody;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\FailedToDetermineOperationType;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\GetMethodSupportsOnlyQueryOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\HttpMethodNotSupported;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidOperationParameter;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidQueryIdParameter;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidQueryParameter;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\MissingContentTypeHeader;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\MissingQueryOrQueryIdParameter;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\PersistedQueriesAreNotSupported;
-use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\UnexpectedContentType;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamInterface;
-
-/**
- * Contains functionality that could be re-used by various server implementations.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\HelperTest
- */
-class Helper
-{
- /**
- * Parses HTTP request using PHP globals and returns Automattic\WooCommerce\Vendor\GraphQL OperationParams
- * contained in this request. For batched requests it returns an array of OperationParams.
- *
- * This function does not check validity of these params
- * (validation is performed separately in validateOperationParams() method).
- *
- * If $readRawBodyFn argument is not provided - will attempt to read raw request body
- * from `php://input` stream.
- *
- * Internally it normalizes input to $method, $bodyParams and $queryParams and
- * calls `parseRequestParams()` to produce actual return value.
- *
- * For PSR-7 request parsing use `parsePsrRequest()` instead.
- *
- * @throws RequestError
- *
- * @return OperationParams|array<int, OperationParams>
- *
- * @api
- */
- public function parseHttpRequest(?callable $readRawBodyFn = null)
- {
- $method = $_SERVER['REQUEST_METHOD'] ?? null;
- $bodyParams = [];
- $urlParams = $_GET;
-
- if ($method === 'POST') {
- $contentType = $_SERVER['CONTENT_TYPE'] ?? null;
-
- if ($contentType === null) {
- throw new MissingContentTypeHeader('Missing "Content-Type" header');
- }
-
- if (stripos($contentType, 'application/graphql') !== false) {
- $rawBody = $readRawBodyFn === null
- ? $this->readRawBody()
- : $readRawBodyFn();
- $bodyParams = ['query' => $rawBody];
- } elseif (stripos($contentType, 'application/json') !== false) {
- $rawBody = $readRawBodyFn === null
- ? $this->readRawBody()
- : $readRawBodyFn();
- $bodyParams = $this->decodeJson($rawBody);
-
- $this->assertJsonObjectOrArray($bodyParams);
- } elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
- $bodyParams = $_POST;
- } elseif (stripos($contentType, 'multipart/form-data') !== false) {
- $bodyParams = $_POST;
- } else {
- throw new UnexpectedContentType('Unexpected content type: ' . Utils::printSafeJson($contentType));
- }
- }
-
- return $this->parseRequestParams($method, $bodyParams, $urlParams);
- }
-
- /**
- * Parses normalized request params and returns instance of OperationParams
- * or array of OperationParams in case of batch operation.
- *
- * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
- *
- * @param array<mixed> $bodyParams
- * @param array<mixed> $queryParams
- *
- * @throws RequestError
- *
- * @return OperationParams|array<int, OperationParams>
- *
- * @api
- */
- public function parseRequestParams(string $method, array $bodyParams, array $queryParams)
- {
- if ($method === 'GET') {
- return OperationParams::create($queryParams, true);
- }
-
- if ($method === 'POST') {
- if (isset($bodyParams[0])) {
- $operations = [];
- foreach ($bodyParams as $entry) {
- $operations[] = OperationParams::create($entry);
- }
-
- return $operations;
- }
-
- return OperationParams::create($bodyParams);
- }
-
- throw new HttpMethodNotSupported("HTTP Method \"{$method}\" is not supported");
- }
-
- /**
- * Checks validity of OperationParams extracted from HTTP request and returns an array of errors
- * if params are invalid (or empty array when params are valid).
- *
- * @return list<RequestError>
- *
- * @api
- */
- public function validateOperationParams(OperationParams $params): array
- {
- $errors = [];
- $query = $params->query ?? '';
- $queryId = $params->queryId ?? '';
- if ($query === '' && $queryId === '') {
- $errors[] = new MissingQueryOrQueryIdParameter('Automattic\WooCommerce\Vendor\GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
- }
-
- if (! is_string($query)) {
- $errors[] = new InvalidQueryParameter(
- 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "query" must be string, but got '
- . Utils::printSafeJson($params->query)
- );
- }
-
- if (! is_string($queryId)) {
- $errors[] = new InvalidQueryIdParameter(
- 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "queryId" must be string, but got '
- . Utils::printSafeJson($params->queryId)
- );
- }
-
- if ($params->operation !== null && ! is_string($params->operation)) {
- $errors[] = new InvalidOperationParameter(
- 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "operation" must be string, but got '
- . Utils::printSafeJson($params->operation)
- );
- }
-
- if ($params->variables !== null && (! is_array($params->variables) || isset($params->variables[0]))) {
- $errors[] = new CannotParseVariables(
- 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got '
- . Utils::printSafeJson($params->originalInput['variables'])
- );
- }
-
- return $errors;
- }
-
- /**
- * Executes Automattic\WooCommerce\Vendor\GraphQL operation with given server configuration and returns execution result
- * (or promise when promise adapter is different from SyncPromiseAdapter).
- *
- * @throws \Exception
- * @throws InvariantViolation
- *
- * @return ExecutionResult|Promise
- *
- * @api
- */
- public function executeOperation(ServerConfig $config, OperationParams $op)
- {
- $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter();
- $result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
-
- if ($promiseAdapter instanceof SyncPromiseAdapter) {
- $result = $promiseAdapter->wait($result);
- }
-
- return $result;
- }
-
- /**
- * Executes batched Automattic\WooCommerce\Vendor\GraphQL operations with shared promise queue
- * (thus, effectively batching deferreds|promises of all queries at once).
- *
- * @param array<OperationParams> $operations
- *
- * @throws \Exception
- * @throws InvariantViolation
- *
- * @return array<int, ExecutionResult>|Promise
- *
- * @api
- */
- public function executeBatch(ServerConfig $config, array $operations)
- {
- $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter();
-
- $result = [];
- foreach ($operations as $operation) {
- $result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true);
- }
-
- $result = $promiseAdapter->all($result);
-
- // Wait for promised results when using sync promises
- if ($promiseAdapter instanceof SyncPromiseAdapter) {
- $result = $promiseAdapter->wait($result);
- }
-
- return $result;
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- protected function promiseToExecuteOperation(
- PromiseAdapter $promiseAdapter,
- ServerConfig $config,
- OperationParams $op,
- bool $isBatch = false
- ): Promise {
- try {
- if ($config->getSchema() === null) {
- throw new InvariantViolation('Schema is required for the server');
- }
-
- if ($isBatch && ! $config->getQueryBatching()) {
- throw new BatchedQueriesAreNotSupported('Batched queries are not supported by this server');
- }
-
- $errors = $this->validateOperationParams($op);
-
- if ($errors !== []) {
- $locatedErrors = array_map(
- [Error::class, 'createLocatedError'],
- $errors
- );
-
- return $promiseAdapter->createFulfilled(
- new ExecutionResult(null, $locatedErrors)
- );
- }
-
- $doc = $op->queryId !== null
- ? $this->loadPersistedQuery($config, $op)
- : $op->query;
-
- if (! $doc instanceof DocumentNode) {
- $doc = Parser::parse($doc);
- }
-
- $operationAST = AST::getOperationAST($doc, $op->operation);
-
- if ($operationAST === null) {
- throw new FailedToDetermineOperationType('Failed to determine operation type');
- }
-
- $operationType = $operationAST->operation;
- if ($operationType !== 'query' && $op->readOnly) {
- throw new GetMethodSupportsOnlyQueryOperation('GET supports only query operation');
- }
-
- $result = GraphQL::promiseToExecute(
- $promiseAdapter,
- $config->getSchema(),
- $doc,
- $this->resolveRootValue($config, $op, $doc, $operationType),
- $this->resolveContextValue($config, $op, $doc, $operationType),
- $op->variables,
- $op->operation,
- $config->getFieldResolver(),
- $this->resolveValidationRules($config, $op, $doc, $operationType)
- );
- } catch (RequestError $e) {
- $result = $promiseAdapter->createFulfilled(
- new ExecutionResult(null, [Error::createLocatedError($e)])
- );
- } catch (Error $e) {
- $result = $promiseAdapter->createFulfilled(
- new ExecutionResult(null, [$e])
- );
- }
-
- $applyErrorHandling = static function (ExecutionResult $result) use ($config): ExecutionResult {
- $result->setErrorsHandler($config->getErrorsHandler());
-
- $result->setErrorFormatter(
- FormattedError::prepareFormatter(
- $config->getErrorFormatter(),
- $config->getDebugFlag()
- )
- );
-
- return $result;
- };
-
- return $result->then($applyErrorHandling);
- }
-
- /**
- * @throws RequestError
- *
- * @return mixed
- */
- protected function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams)
- {
- $loader = $config->getPersistedQueryLoader();
-
- if ($loader === null) {
- throw new PersistedQueriesAreNotSupported('Persisted queries are not supported by this server');
- }
-
- $source = $loader($operationParams->queryId, $operationParams);
-
- // @phpstan-ignore-next-line Necessary until PHP gains function types
- if (! is_string($source) && ! $source instanceof DocumentNode) {
- $documentNode = DocumentNode::class;
- $safeSource = Utils::printSafe($source);
- throw new InvariantViolation("Persisted query loader must return query string or instance of {$documentNode} but got: {$safeSource}");
- }
-
- return $source;
- }
-
- /** @return array<mixed>|null */
- protected function resolveValidationRules(
- ServerConfig $config,
- OperationParams $params,
- DocumentNode $doc,
- string $operationType
- ): ?array {
- $validationRules = $config->getValidationRules();
-
- if (is_callable($validationRules)) {
- $validationRules = $validationRules($params, $doc, $operationType);
- }
-
- // @phpstan-ignore-next-line unless PHP gains function types, we have to check this at runtime
- if ($validationRules !== null && ! is_array($validationRules)) {
- $safeValidationRules = Utils::printSafe($validationRules);
- throw new InvariantViolation("Expecting validation rules to be array or callable returning array, but got: {$safeValidationRules}");
- }
-
- return $validationRules;
- }
-
- /** @return mixed */
- protected function resolveRootValue(
- ServerConfig $config,
- OperationParams $params,
- DocumentNode $doc,
- string $operationType
- ) {
- $rootValue = $config->getRootValue();
-
- if (is_callable($rootValue)) {
- $rootValue = $rootValue($params, $doc, $operationType);
- }
-
- return $rootValue;
- }
-
- /** @return mixed user defined */
- protected function resolveContextValue(
- ServerConfig $config,
- OperationParams $params,
- DocumentNode $doc,
- string $operationType
- ) {
- $context = $config->getContext();
-
- if (is_callable($context)) {
- $context = $context($params, $doc, $operationType);
- }
-
- return $context;
- }
-
- /**
- * Send response using standard PHP `header()` and `echo`.
- *
- * @param Promise|ExecutionResult|array<ExecutionResult> $result
- *
- * @api
- *
- * @throws \JsonException
- */
- public function sendResponse($result): void
- {
- if ($result instanceof Promise) {
- $result->then(function ($actualResult): void {
- $this->emitResponse($actualResult);
- });
- } else {
- $this->emitResponse($result);
- }
- }
-
- /**
- * @param array<mixed>|\JsonSerializable $jsonSerializable
- *
- * @throws \JsonException
- */
- protected function emitResponse($jsonSerializable): void
- {
- header('Content-Type: application/json;charset=utf-8');
- echo json_encode($jsonSerializable, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
- }
-
- /** @throws RequestError */
- protected function readRawBody(): string
- {
- $body = file_get_contents('php://input');
- if ($body === false) {
- throw new CannotReadBody('Cannot not read body.');
- }
-
- return $body;
- }
-
- /**
- * Converts PSR-7 request to OperationParams or an array thereof.
- *
- * @throws RequestError
- *
- * @return OperationParams|array<OperationParams>
- *
- * @api
- */
- public function parsePsrRequest(RequestInterface $request)
- {
- if ($request->getMethod() === 'GET') {
- $bodyParams = [];
- } else {
- $contentType = $request->getHeader('content-type');
-
- if (! isset($contentType[0])) {
- throw new MissingContentTypeHeader('Missing "Content-Type" header');
- }
-
- if (stripos($contentType[0], 'application/graphql') !== false) {
- $bodyParams = ['query' => (string) $request->getBody()];
- } elseif (stripos($contentType[0], 'application/json') !== false) {
- $bodyParams = $request instanceof ServerRequestInterface
- ? $request->getParsedBody()
- : $this->decodeJson((string) $request->getBody());
-
- $this->assertJsonObjectOrArray($bodyParams);
- } else {
- if ($request instanceof ServerRequestInterface) {
- $bodyParams = $request->getParsedBody();
- }
-
- $bodyParams ??= $this->decodeContent((string) $request->getBody());
- }
- }
-
- parse_str(html_entity_decode($request->getUri()->getQuery()), $queryParams);
-
- return $this->parseRequestParams(
- $request->getMethod(),
- $bodyParams,
- $queryParams
- );
- }
-
- /**
- * @throws RequestError
- *
- * @return mixed
- */
- protected function decodeJson(string $rawBody)
- {
- $bodyParams = json_decode($rawBody, true);
-
- if (json_last_error() !== \JSON_ERROR_NONE) {
- throw new CannotParseJsonBody('Expected JSON object or array for "application/json" request, but failed to parse because: ' . json_last_error_msg());
- }
-
- return $bodyParams;
- }
-
- /** @return array<mixed> */
- protected function decodeContent(string $rawBody): array
- {
- parse_str($rawBody, $bodyParams);
-
- return $bodyParams;
- }
-
- /**
- * @param mixed $bodyParams
- *
- * @throws RequestError
- */
- protected function assertJsonObjectOrArray($bodyParams): void
- {
- if (! is_array($bodyParams)) {
- $notArray = Utils::printSafeJson($bodyParams);
- throw new CannotParseJsonBody("Expected JSON object or array for \"application/json\" request, got: {$notArray}");
- }
- }
-
- /**
- * Converts query execution result to PSR-7 response.
- *
- * @param Promise|ExecutionResult|array<ExecutionResult> $result
- *
- * @throws \InvalidArgumentException
- * @throws \JsonException
- * @throws \RuntimeException
- *
- * @return Promise|ResponseInterface
- *
- * @api
- */
- public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
- {
- if ($result instanceof Promise) {
- return $result->then(
- fn ($actualResult): ResponseInterface => $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream)
- );
- }
-
- return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
- }
-
- /**
- * @param ExecutionResult|array<ExecutionResult> $result
- *
- * @throws \InvalidArgumentException
- * @throws \JsonException
- * @throws \RuntimeException
- */
- protected function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream): ResponseInterface
- {
- $writableBodyStream->write(json_encode($result, JSON_THROW_ON_ERROR));
-
- return $response
- ->withHeader('Content-Type', 'application/json')
- ->withBody($writableBodyStream);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php b/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php
deleted file mode 100644
index d16685f9dd3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
-
-/**
- * Structure representing parsed HTTP parameters for Automattic\WooCommerce\Vendor\GraphQL operation.
- *
- * The properties in this class are not strictly typed, as this class
- * is only meant to serve as an intermediary representation which is
- * not yet validated.
- */
-class OperationParams
-{
- /**
- * Id of the query (when using persisted queries).
- *
- * Valid aliases (case-insensitive):
- * - id
- * - queryId
- * - documentId
- *
- * @api
- *
- * @var mixed should be string|null
- */
- public $queryId;
-
- /**
- * A document containing Automattic\WooCommerce\Vendor\GraphQL operations and fragments to execute.
- *
- * @api
- *
- * @var mixed should be string|null
- */
- public $query;
-
- /**
- * The name of the operation in the document to execute.
- *
- * @api
- *
- * @var mixed should be string|null
- */
- public $operation;
-
- /**
- * Values for any variables defined by the operation.
- *
- * @api
- *
- * @var mixed should be array<string, mixed>
- */
- public $variables;
-
- /**
- * Reserved for implementors to extend the protocol however they see fit.
- *
- * @api
- *
- * @var mixed should be array<string, mixed>
- */
- public $extensions;
-
- /**
- * Executed in read-only context (e.g. via HTTP GET request)?
- *
- * @api
- */
- public bool $readOnly;
-
- /**
- * The raw params used to construct this instance.
- *
- * @api
- *
- * @var array<string, mixed>
- */
- public array $originalInput;
-
- /**
- * Creates an instance from given array.
- *
- * @param array<string, mixed> $params
- *
- * @api
- */
- public static function create(array $params, bool $readonly = false): OperationParams
- {
- $instance = new static();
-
- $params = array_change_key_case($params, \CASE_LOWER);
- $instance->originalInput = $params;
-
- $params += [
- 'query' => null,
- 'queryid' => null,
- 'documentid' => null, // alias to queryid
- 'id' => null, // alias to queryid
- 'operationname' => null,
- 'variables' => null,
- 'extensions' => null,
- ];
-
- foreach ($params as &$value) {
- if ($value === '') {
- $value = null;
- }
- }
-
- $instance->query = $params['query'];
- $instance->queryId = $params['queryid'] ?? $params['documentid'] ?? $params['id'];
- $instance->operation = $params['operationname'];
- $instance->variables = static::decodeIfJSON($params['variables']);
- $instance->extensions = static::decodeIfJSON($params['extensions']);
- $instance->readOnly = $readonly;
-
- // Apollo server/client compatibility
- if (
- isset($instance->extensions['persistedQuery']['sha256Hash'])
- && $instance->queryId === null
- ) {
- $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash'];
- }
-
- return $instance;
- }
-
- /**
- * Decodes the value if it is JSON, otherwise returns it unchanged.
- *
- * @param mixed $value
- *
- * @return mixed
- */
- protected static function decodeIfJSON($value)
- {
- if (! is_string($value)) {
- return $value;
- }
-
- $decoded = json_decode($value, true);
- if (json_last_error() === \JSON_ERROR_NONE) {
- return $decoded;
- }
-
- return $value;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php b/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php
deleted file mode 100644
index 829115cb022..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware;
-
-class RequestError extends \Exception implements ClientAware
-{
- public function isClientSafe(): bool
- {
- return true;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php b/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php
deleted file mode 100644
index 9317e61c25a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php
+++ /dev/null
@@ -1,347 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
-
-/**
- * Server configuration class.
- * Could be passed directly to server constructor. List of options accepted by **create** method is
- * [described in docs](executing-queries.md#server-configuration-options).
- *
- * Usage example:
- *
- * $config = Automattic\WooCommerce\Vendor\GraphQL\Server\ServerConfig::create()
- * ->setSchema($mySchema)
- * ->setContext($myContext);
- *
- * $server = new Automattic\WooCommerce\Vendor\GraphQL\Server\StandardServer($config);
- *
- * @see ExecutionResult
- *
- * @phpstan-type PersistedQueryLoader callable(string $queryId, OperationParams $operation): (string|DocumentNode)
- * @phpstan-type RootValueResolver callable(OperationParams $operation, DocumentNode $doc, string $operationType): mixed
- * @phpstan-type ValidationRulesOption array<ValidationRule>|null|callable(OperationParams $operation, DocumentNode $doc, string $operationType): array<ValidationRule>
- *
- * @phpstan-import-type ErrorsHandler from ExecutionResult
- * @phpstan-import-type ErrorFormatter from ExecutionResult
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\ServerConfigTest
- */
-class ServerConfig
-{
- /**
- * Converts an array of options to instance of ServerConfig
- * (or just returns empty config when array is not passed).
- *
- * @param array<string, mixed> $config
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public static function create(array $config = []): self
- {
- $instance = new static();
- foreach ($config as $key => $value) {
- switch ($key) {
- case 'schema':
- $instance->setSchema($value);
- break;
- case 'rootValue':
- $instance->setRootValue($value);
- break;
- case 'context':
- $instance->setContext($value);
- break;
- case 'fieldResolver':
- $instance->setFieldResolver($value);
- break;
- case 'validationRules':
- $instance->setValidationRules($value);
- break;
- case 'queryBatching':
- $instance->setQueryBatching($value);
- break;
- case 'debugFlag':
- $instance->setDebugFlag($value);
- break;
- case 'persistedQueryLoader':
- $instance->setPersistedQueryLoader($value);
- break;
- case 'errorFormatter':
- $instance->setErrorFormatter($value);
- break;
- case 'errorsHandler':
- $instance->setErrorsHandler($value);
- break;
- case 'promiseAdapter':
- $instance->setPromiseAdapter($value);
- break;
- default:
- throw new InvariantViolation("Unknown server config option: {$key}");
- }
- }
-
- return $instance;
- }
-
- private ?Schema $schema = null;
-
- /** @var mixed|callable(self, OperationParams, DocumentNode): mixed|null */
- private $context;
-
- /**
- * @var mixed|callable
- *
- * @phpstan-var mixed|RootValueResolver
- */
- private $rootValue;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ErrorFormatter|null
- */
- private $errorFormatter;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ErrorsHandler|null
- */
- private $errorsHandler;
-
- private int $debugFlag = DebugFlag::NONE;
-
- private bool $queryBatching = false;
-
- /**
- * @var array<ValidationRule>|callable|null
- *
- * @phpstan-var ValidationRulesOption
- */
- private $validationRules;
-
- /** @var callable|null */
- private $fieldResolver;
-
- private ?PromiseAdapter $promiseAdapter = null;
-
- /**
- * @var callable|null
- *
- * @phpstan-var PersistedQueryLoader|null
- */
- private $persistedQueryLoader;
-
- /** @api */
- public function setSchema(Schema $schema): self
- {
- $this->schema = $schema;
-
- return $this;
- }
-
- /**
- * @param mixed|callable $context
- *
- * @api
- */
- public function setContext($context): self
- {
- $this->context = $context;
-
- return $this;
- }
-
- /**
- * @param mixed|callable $rootValue
- *
- * @phpstan-param mixed|RootValueResolver $rootValue
- *
- * @api
- */
- public function setRootValue($rootValue): self
- {
- $this->rootValue = $rootValue;
-
- return $this;
- }
-
- /**
- * @phpstan-param ErrorFormatter $errorFormatter
- *
- * @api
- */
- public function setErrorFormatter(callable $errorFormatter): self
- {
- $this->errorFormatter = $errorFormatter;
-
- return $this;
- }
-
- /**
- * @phpstan-param ErrorsHandler $handler
- *
- * @api
- */
- public function setErrorsHandler(callable $handler): self
- {
- $this->errorsHandler = $handler;
-
- return $this;
- }
-
- /**
- * Set validation rules for this server.
- *
- * @param array<ValidationRule>|callable|null $validationRules
- *
- * @phpstan-param ValidationRulesOption $validationRules
- *
- * @api
- */
- public function setValidationRules($validationRules): self
- {
- // @phpstan-ignore-next-line necessary until we can use proper union types
- if (! is_array($validationRules) && ! is_callable($validationRules) && $validationRules !== null) {
- $invalidValidationRules = Utils::printSafe($validationRules);
- throw new InvariantViolation("Server config expects array of validation rules or callable returning such array, but got {$invalidValidationRules}");
- }
-
- $this->validationRules = $validationRules;
-
- return $this;
- }
-
- /** @api */
- public function setFieldResolver(callable $fieldResolver): self
- {
- $this->fieldResolver = $fieldResolver;
-
- return $this;
- }
-
- /**
- * @phpstan-param PersistedQueryLoader|null $persistedQueryLoader
- *
- * @api
- */
- public function setPersistedQueryLoader(?callable $persistedQueryLoader): self
- {
- $this->persistedQueryLoader = $persistedQueryLoader;
-
- return $this;
- }
-
- /**
- * Set response debug flags.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag class for a list of all available flags
- *
- * @api
- */
- public function setDebugFlag(int $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE): self
- {
- $this->debugFlag = $debugFlag;
-
- return $this;
- }
-
- /**
- * Allow batching queries (disabled by default).
- *
- * @api
- */
- public function setQueryBatching(bool $enableBatching): self
- {
- $this->queryBatching = $enableBatching;
-
- return $this;
- }
-
- /** @api */
- public function setPromiseAdapter(PromiseAdapter $promiseAdapter): self
- {
- $this->promiseAdapter = $promiseAdapter;
-
- return $this;
- }
-
- /** @return mixed|callable */
- public function getContext()
- {
- return $this->context;
- }
-
- /**
- * @return mixed|callable
- *
- * @phpstan-return mixed|RootValueResolver
- */
- public function getRootValue()
- {
- return $this->rootValue;
- }
-
- public function getSchema(): ?Schema
- {
- return $this->schema;
- }
-
- /** @phpstan-return ErrorFormatter|null */
- public function getErrorFormatter(): ?callable
- {
- return $this->errorFormatter;
- }
-
- /** @phpstan-return ErrorsHandler|null */
- public function getErrorsHandler(): ?callable
- {
- return $this->errorsHandler;
- }
-
- public function getPromiseAdapter(): ?PromiseAdapter
- {
- return $this->promiseAdapter;
- }
-
- /**
- * @return array<ValidationRule>|callable|null
- *
- * @phpstan-return ValidationRulesOption
- */
- public function getValidationRules()
- {
- return $this->validationRules;
- }
-
- public function getFieldResolver(): ?callable
- {
- return $this->fieldResolver;
- }
-
- /** @phpstan-return PersistedQueryLoader|null */
- public function getPersistedQueryLoader(): ?callable
- {
- return $this->persistedQueryLoader;
- }
-
- public function getDebugFlag(): int
- {
- return $this->debugFlag;
- }
-
- public function getQueryBatching(): bool
- {
- return $this->queryBatching;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php b/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php
deleted file mode 100644
index bbec3fbda8d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\StreamInterface;
-
-/**
- * Automattic\WooCommerce\Vendor\GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql)
- * and [Apollo Server](https://github.com/apollographql/graphql-server).
- * Usage Example:.
- *
- * $server = new StandardServer([
- * 'schema' => $mySchema
- * ]);
- * $server->handleRequest();
- *
- * Or using [ServerConfig](class-reference.md#graphqlserverserverconfig) instance:
- *
- * $config = Automattic\WooCommerce\Vendor\GraphQL\Server\ServerConfig::create()
- * ->setSchema($mySchema)
- * ->setContext($myContext);
- *
- * $server = new Automattic\WooCommerce\Vendor\GraphQL\Server\StandardServer($config);
- * $server->handleRequest();
- *
- * See [dedicated section in docs](executing-queries.md#using-server) for details.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\StandardServerTest
- */
-class StandardServer
-{
- protected ServerConfig $config;
-
- protected Helper $helper;
-
- /**
- * @param ServerConfig|array<string, mixed> $config
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public function __construct($config)
- {
- if (is_array($config)) {
- $config = ServerConfig::create($config);
- }
-
- // @phpstan-ignore-next-line necessary until we can use proper union types
- if (! $config instanceof ServerConfig) {
- $safeConfig = Utils::printSafe($config);
- throw new InvariantViolation("Expecting valid server config, but got {$safeConfig}");
- }
-
- $this->config = $config;
- $this->helper = new Helper();
- }
-
- /**
- * Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`).
- *
- * When $parsedBody is not set, it uses PHP globals to parse a request.
- * It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
- * and then pass it to the server.
- *
- * See `executeRequest()` if you prefer to emit the response yourself
- * (e.g. using the Response object of some framework).
- *
- * @param OperationParams|array<OperationParams> $parsedBody
- *
- * @api
- *
- * @throws \Exception
- * @throws InvariantViolation
- * @throws RequestError
- */
- public function handleRequest($parsedBody = null): void
- {
- $result = $this->executeRequest($parsedBody);
- $this->helper->sendResponse($result);
- }
-
- /**
- * Executes a Automattic\WooCommerce\Vendor\GraphQL operation and returns an execution result
- * (or promise when promise adapter is different from SyncPromiseAdapter).
- *
- * When $parsedBody is not set, it uses PHP globals to parse a request.
- * It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
- * and then pass it to the server.
- *
- * PSR-7 compatible method executePsrRequest() does exactly this.
- *
- * @param OperationParams|array<OperationParams> $parsedBody
- *
- * @throws \Exception
- * @throws InvariantViolation
- * @throws RequestError
- *
- * @return ExecutionResult|array<int, ExecutionResult>|Promise
- *
- * @api
- */
- public function executeRequest($parsedBody = null)
- {
- if ($parsedBody === null) {
- $parsedBody = $this->helper->parseHttpRequest();
- }
-
- if (is_array($parsedBody)) {
- return $this->helper->executeBatch($this->config, $parsedBody);
- }
-
- return $this->helper->executeOperation($this->config, $parsedBody);
- }
-
- /**
- * Executes PSR-7 request and fulfills PSR-7 response.
- *
- * See `executePsrRequest()` if you prefer to create response yourself
- * (e.g. using specific JsonResponse instance of some framework).
- *
- * @throws \Exception
- * @throws \InvalidArgumentException
- * @throws \JsonException
- * @throws \RuntimeException
- * @throws InvariantViolation
- * @throws RequestError
- *
- * @return ResponseInterface|Promise
- *
- * @api
- */
- public function processPsrRequest(
- RequestInterface $request,
- ResponseInterface $response,
- StreamInterface $writableBodyStream
- ) {
- $result = $this->executePsrRequest($request);
-
- return $this->helper->toPsrResponse($result, $response, $writableBodyStream);
- }
-
- /**
- * Executes Automattic\WooCommerce\Vendor\GraphQL operation and returns execution result
- * (or promise when promise adapter is different from SyncPromiseAdapter).
- *
- * @throws \Exception
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws RequestError
- *
- * @return ExecutionResult|array<int, ExecutionResult>|Promise
- *
- * @api
- */
- public function executePsrRequest(RequestInterface $request)
- {
- $parsedBody = $this->helper->parsePsrRequest($request);
-
- return $this->executeRequest($parsedBody);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php
deleted file mode 100644
index 083a6e4d608..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
-
-/**
- * @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|Deferred|null
- * @phpstan-type ResolveType callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): ResolveTypeReturn
- * @phpstan-type ResolveValue callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): mixed
- */
-interface AbstractType
-{
- /**
- * Receives the original resolved value and transforms it if necessary.
- *
- * This will be called before `resolveType`.
- *
- * @param mixed $objectValue The resolved value for the object type
- * @param mixed $context The context that was passed to GraphQL::execute()
- *
- * @return mixed The possibly transformed value
- */
- public function resolveValue($objectValue, $context, ResolveInfo $info);
-
- /**
- * Resolves the concrete ObjectType for the given value.
- *
- * This will be called after `resolveValue`.
- *
- * @param mixed $objectValue The resolved value for the object type
- * @param mixed $context The context that was passed to GraphQL::execute()
- *
- * @return ObjectType|string|callable|Deferred|null
- *
- * @phpstan-return ResolveTypeReturn
- */
- public function resolveType($objectValue, $context, ResolveInfo $info);
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php
deleted file mode 100644
index 68a1ae9e0d7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType)
- * @phpstan-type UnnamedArgumentConfig array{
- * name?: string,
- * type: ArgumentType,
- * defaultValue?: mixed,
- * description?: string|null,
- * deprecationReason?: string|null,
- * astNode?: InputValueDefinitionNode|null
- * }
- * @phpstan-type ArgumentConfig array{
- * name: string,
- * type: ArgumentType,
- * defaultValue?: mixed,
- * description?: string|null,
- * deprecationReason?: string|null,
- * astNode?: InputValueDefinitionNode|null
- * }
- * @phpstan-type ArgumentListConfig iterable<ArgumentConfig|ArgumentType>|iterable<UnnamedArgumentConfig>
- */
-class Argument
-{
- public string $name;
-
- /** @var mixed */
- public $defaultValue;
-
- public ?string $description;
-
- public ?string $deprecationReason;
-
- /** @var Type&InputType */
- private Type $type;
-
- public ?InputValueDefinitionNode $astNode;
-
- /** @phpstan-var ArgumentConfig */
- public array $config;
-
- /** @phpstan-param ArgumentConfig $config */
- public function __construct(array $config)
- {
- $this->name = $config['name'];
- $this->defaultValue = $config['defaultValue'] ?? null;
- $this->description = $config['description'] ?? null;
- $this->deprecationReason = $config['deprecationReason'] ?? null;
- // Do nothing for type, it is lazy loaded in getType()
- $this->astNode = $config['astNode'] ?? null;
-
- $this->config = $config;
- }
-
- /**
- * @phpstan-param ArgumentListConfig $config
- *
- * @return array<int, self>
- */
- public static function listFromConfig(iterable $config): array
- {
- $list = [];
-
- foreach ($config as $name => $argConfig) {
- if (! is_array($argConfig)) {
- $argConfig = ['type' => $argConfig];
- }
-
- /** @phpstan-var ArgumentConfig $argConfigWithName */
- $argConfigWithName = $argConfig + ['name' => $name];
-
- $list[] = new self($argConfigWithName);
- }
-
- return $list;
- }
-
- /** @return Type&InputType */
- public function getType(): Type
- {
- if (! isset($this->type)) {
- $this->type = Schema::resolveType($this->config['type']);
- }
-
- return $this->type;
- }
-
- public function defaultValueExists(): bool
- {
- return array_key_exists('defaultValue', $this->config);
- }
-
- public function isRequired(): bool
- {
- return $this->getType() instanceof NonNull
- && ! $this->defaultValueExists();
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->deprecationReason;
- }
-
- /**
- * @param Type&NamedType $parentType
- *
- * @throws InvariantViolation
- */
- public function assertValid(FieldDefinition $parentField, Type $parentType): void
- {
- $error = Utils::isValidNameError($this->name);
- if ($error !== null) {
- throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}:) {$error->getMessage()}");
- }
-
- $type = Type::getNamedType($this->getType());
-
- if (! $type instanceof InputType) {
- $notInputType = Utils::printSafe($this->type);
- throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}): argument type must be Input Type but got: {$notInputType}");
- }
-
- if ($this->isRequired() && $this->isDeprecated()) {
- throw new InvariantViolation("Required argument {$parentType->name}.{$parentField->name}({$this->name}:) cannot be deprecated.");
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php
deleted file mode 100644
index 4f7e8b1ccd6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class BooleanType extends ScalarType
-{
- public string $name = Type::BOOLEAN;
-
- public ?string $description = 'The `Boolean` scalar type represents `true` or `false`.';
-
- /**
- * Serialize the given value to a Boolean.
- *
- * The Automattic\WooCommerce\Vendor\GraphQL spec leaves this up to the implementations, so we just do what
- * PHP does natively to make this intuitive for developers.
- */
- public function serialize($value): bool
- {
- return (bool) $value;
- }
-
- /** @throws Error */
- public function parseValue($value): bool
- {
- if (is_bool($value)) {
- return $value;
- }
-
- $notBoolean = Utils::printSafeJson($value);
- throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}");
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null): bool
- {
- if ($valueNode instanceof BooleanValueNode) {
- return $valueNode->value;
- }
-
- $notBoolean = Printer::doPrint($valueNode);
- throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}", $valueNode);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php
deleted file mode 100644
index 1167bb5e09a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/*
-export type GraphQLCompositeType =
-GraphQLObjectType |
-GraphQLInterfaceType |
-GraphQLUnionType;
-*/
-
-interface CompositeType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php
deleted file mode 100644
index f6845b0a507..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-type InputCustomScalarConfig array{
- * name?: string|null,
- * description?: string|null,
- * serialize?: callable(mixed): mixed,
- * parseValue: callable(mixed): mixed,
- * parseLiteral: callable(ValueNode&Node, array<string, mixed>|null): mixed,
- * astNode?: ScalarTypeDefinitionNode|null,
- * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
- * }
- * @phpstan-type OutputCustomScalarConfig array{
- * name?: string|null,
- * description?: string|null,
- * serialize: callable(mixed): mixed,
- * parseValue?: callable(mixed): mixed,
- * parseLiteral?: callable(ValueNode&Node, array<string, mixed>|null): mixed,
- * astNode?: ScalarTypeDefinitionNode|null,
- * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
- * }
- * @phpstan-type CustomScalarConfig InputCustomScalarConfig|OutputCustomScalarConfig
- */
-class CustomScalarType extends ScalarType
-{
- /** @phpstan-var CustomScalarConfig */
- // @phpstan-ignore-next-line specialize type
- public array $config;
-
- /**
- * @param array<string, mixed> $config
- *
- * @phpstan-param CustomScalarConfig $config
- */
- public function __construct(array $config)
- {
- parent::__construct($config);
- }
-
- public function serialize($value)
- {
- if (isset($this->config['serialize'])) {
- return $this->config['serialize']($value);
- }
-
- return $value;
- }
-
- public function parseValue($value)
- {
- if (isset($this->config['parseValue'])) {
- return $this->config['parseValue']($value);
- }
-
- return $value;
- }
-
- /** @throws \Exception */
- public function parseLiteral(Node $valueNode, ?array $variables = null)
- {
- if (isset($this->config['parseLiteral'])) {
- return $this->config['parseLiteral']($valueNode, $variables);
- }
-
- return AST::valueFromASTUntyped($valueNode, $variables);
- }
-
- /**
- * @throws Error
- * @throws InvariantViolation
- */
- public function assertValid(): void
- {
- parent::assertValid();
-
- $serialize = $this->config['serialize'] ?? null;
- $parseValue = $this->config['parseValue'] ?? null;
- $parseLiteral = $this->config['parseLiteral'] ?? null;
-
- $hasSerialize = $serialize !== null;
- $hasParseValue = $parseValue !== null;
- $hasParseLiteral = $parseLiteral !== null;
- $hasParse = $hasParseValue && $hasParseLiteral;
-
- if ($hasParseValue !== $hasParseLiteral) {
- throw new InvariantViolation("{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions to work as an input type.");
- }
-
- if (! $hasSerialize && ! $hasParse) {
- throw new InvariantViolation("{$this->name} must provide \"parseValue\" and \"parseLiteral\" functions, \"serialize\" function, or both.");
- }
-
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if ($hasSerialize && ! is_callable($serialize)) {
- $notCallable = Utils::printSafe($serialize);
- throw new InvariantViolation("{$this->name} must provide \"serialize\" as a callable if given, but got: {$notCallable}.");
- }
-
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if ($hasParseValue && ! is_callable($parseValue)) {
- $notCallable = Utils::printSafe($parseValue);
- throw new InvariantViolation("{$this->name} must provide \"parseValue\" as a callable if given, but got: {$notCallable}.");
- }
-
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if ($hasParseLiteral && ! is_callable($parseLiteral)) {
- $notCallable = Utils::printSafe($parseLiteral);
- throw new InvariantViolation("{$this->name} must provide \"parseLiteral\" as a callable if given, but got: {$notCallable}.");
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php
deleted file mode 100644
index e9798bcd06c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-#[\Attribute(\Attribute::TARGET_ALL)]
-class Deprecated
-{
- public string $reason;
-
- public function __construct(string $reason = Directive::DEFAULT_DEPRECATION_REASON)
- {
- $this->reason = $reason;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php
deleted file mode 100644
index 96d59c7c3f8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-#[\Attribute(\Attribute::TARGET_ALL)]
-class Description
-{
- public string $description;
-
- public function __construct(string $description)
- {
- $this->description = $description;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php
deleted file mode 100644
index 0395ab3a8c4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php
+++ /dev/null
@@ -1,185 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
-
-/**
- * @phpstan-import-type ArgumentListConfig from Argument
- *
- * @phpstan-type DirectiveConfig array{
- * name: string,
- * description?: string|null,
- * args?: ArgumentListConfig|null,
- * locations: array<string>,
- * isRepeatable?: bool|null,
- * astNode?: DirectiveDefinitionNode|null
- * }
- */
-class Directive
-{
- public const DEFAULT_DEPRECATION_REASON = 'No longer supported';
-
- public const INCLUDE_NAME = 'include';
- public const IF_ARGUMENT_NAME = 'if';
- public const SKIP_NAME = 'skip';
- public const DEPRECATED_NAME = 'deprecated';
- public const REASON_ARGUMENT_NAME = 'reason';
- public const ONE_OF_NAME = 'oneOf';
-
- /**
- * Lazily initialized.
- *
- * @var array<string, Directive>|null
- */
- protected static ?array $internalDirectives = null;
-
- public string $name;
-
- public ?string $description;
-
- /** @var array<int, Argument> */
- public array $args;
-
- public bool $isRepeatable;
-
- /** @var array<string> */
- public array $locations;
-
- public ?DirectiveDefinitionNode $astNode;
-
- /**
- * @var array<string, mixed>
- *
- * @phpstan-var DirectiveConfig
- */
- public array $config;
-
- /**
- * @param array<string, mixed> $config
- *
- * @phpstan-param DirectiveConfig $config
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'];
- $this->description = $config['description'] ?? null;
- $this->args = isset($config['args'])
- ? Argument::listFromConfig($config['args'])
- : [];
- $this->isRepeatable = $config['isRepeatable'] ?? false;
- $this->locations = $config['locations'];
- $this->astNode = $config['astNode'] ?? null;
-
- $this->config = $config;
- }
-
- /** @return array<string, Directive> */
- public static function builtInDirectives(): array
- {
- return [
- self::INCLUDE_NAME => self::includeDirective(),
- self::SKIP_NAME => self::skipDirective(),
- self::DEPRECATED_NAME => self::deprecatedDirective(),
- self::ONE_OF_NAME => self::oneOfDirective(),
- ];
- }
-
- /**
- * @deprecated use {@see Directive::builtInDirectives()}
- *
- * @return array<string, Directive>
- */
- public static function getInternalDirectives(): array
- {
- return self::builtInDirectives();
- }
-
- public static function includeDirective(): Directive
- {
- return self::$internalDirectives[self::INCLUDE_NAME] ??= new self([
- 'name' => self::INCLUDE_NAME,
- 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.',
- 'locations' => [
- DirectiveLocation::FIELD,
- DirectiveLocation::FRAGMENT_SPREAD,
- DirectiveLocation::INLINE_FRAGMENT,
- ],
- 'args' => [
- self::IF_ARGUMENT_NAME => [
- 'type' => Type::nonNull(Type::boolean()),
- 'description' => 'Included when true.',
- ],
- ],
- ]);
- }
-
- public static function skipDirective(): Directive
- {
- return self::$internalDirectives[self::SKIP_NAME] ??= new self([
- 'name' => self::SKIP_NAME,
- 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.',
- 'locations' => [
- DirectiveLocation::FIELD,
- DirectiveLocation::FRAGMENT_SPREAD,
- DirectiveLocation::INLINE_FRAGMENT,
- ],
- 'args' => [
- self::IF_ARGUMENT_NAME => [
- 'type' => Type::nonNull(Type::boolean()),
- 'description' => 'Skipped when true.',
- ],
- ],
- ]);
- }
-
- public static function deprecatedDirective(): Directive
- {
- return self::$internalDirectives[self::DEPRECATED_NAME] ??= new self([
- 'name' => self::DEPRECATED_NAME,
- 'description' => 'Marks an element of a Automattic\WooCommerce\Vendor\GraphQL schema as no longer supported.',
- 'locations' => [
- DirectiveLocation::FIELD_DEFINITION,
- DirectiveLocation::ENUM_VALUE,
- DirectiveLocation::ARGUMENT_DEFINITION,
- DirectiveLocation::INPUT_FIELD_DEFINITION,
- ],
- 'args' => [
- self::REASON_ARGUMENT_NAME => [
- 'type' => Type::string(),
- 'description' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).',
- 'defaultValue' => self::DEFAULT_DEPRECATION_REASON,
- ],
- ],
- ]);
- }
-
- public static function oneOfDirective(): Directive
- {
- return self::$internalDirectives[self::ONE_OF_NAME] ??= new self([
- 'name' => self::ONE_OF_NAME,
- 'description' => 'Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its fields be provided).',
- 'locations' => [
- DirectiveLocation::INPUT_OBJECT,
- ],
- 'args' => [],
- ]);
- }
-
- public static function isBuiltInDirective(self $directive): bool
- {
- return array_key_exists($directive->name, self::builtInDirectives());
- }
-
- /** @deprecated use {@see Directive::isBuiltInDirective()} */
- public static function isSpecifiedDirective(Directive $directive): bool
- {
- return self::isBuiltInDirective($directive);
- }
-
- public static function resetCachedInstances(): void
- {
- self::$internalDirectives = null;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php
deleted file mode 100644
index b10e881d034..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php
+++ /dev/null
@@ -1,270 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\MixedStore;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @see EnumValueDefinitionNode
- *
- * @phpstan-type PartialEnumValueConfig array{
- * name?: string,
- * value?: mixed,
- * deprecationReason?: string|null,
- * description?: string|null,
- * astNode?: EnumValueDefinitionNode|null
- * }
- * @phpstan-type EnumValues iterable<string, PartialEnumValueConfig>|iterable<string, mixed>|iterable<int, string>
- * @phpstan-type EnumTypeConfig array{
- * name?: string|null,
- * description?: string|null,
- * values: EnumValues|callable(): EnumValues,
- * astNode?: EnumTypeDefinitionNode|null,
- * extensionASTNodes?: array<EnumTypeExtensionNode>|null
- * }
- */
-class EnumType extends Type implements InputType, OutputType, LeafType, NullableType, NamedType
-{
- use NamedTypeImplementation;
-
- public ?EnumTypeDefinitionNode $astNode;
-
- /** @var array<EnumTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /** @phpstan-var EnumTypeConfig */
- public array $config;
-
- /**
- * Lazily initialized.
- *
- * @var array<int, EnumValueDefinition>
- */
- private array $values;
-
- /**
- * Lazily initialized.
- *
- * @var MixedStore<EnumValueDefinition>
- */
- private MixedStore $valueLookup;
-
- /** @var array<string, EnumValueDefinition> */
- private array $nameLookup;
-
- /**
- * @phpstan-param EnumTypeConfig $config
- *
- * @throws InvariantViolation
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- /** @throws InvariantViolation */
- public function getValue(string $name): ?EnumValueDefinition
- {
- if (! isset($this->nameLookup)) {
- $this->initializeNameLookup();
- }
-
- return $this->nameLookup[$name] ?? null;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, EnumValueDefinition>
- */
- public function getValues(): array
- {
- if (! isset($this->values)) {
- $this->values = [];
-
- $values = $this->config['values'];
- if (is_callable($values)) {
- $values = $values();
- }
-
- // We are just assuming the config option is set correctly here, validation happens in assertValid()
- foreach ($values as $name => $value) {
- if (is_string($name)) {
- if (is_array($value)) {
- $value += ['name' => $name, 'value' => $name];
- } else {
- $value = ['name' => $name, 'value' => $value];
- }
- } elseif (is_string($value)) {
- $value = ['name' => $value, 'value' => $value];
- } else {
- throw new InvariantViolation("{$this->name} values must be an array with value names as keys or values.");
- }
-
- // @phpstan-ignore-next-line assume the config matches
- $this->values[] = new EnumValueDefinition($value);
- }
- }
-
- return $this->values;
- }
-
- /**
- * @throws \InvalidArgumentException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- public function serialize($value)
- {
- $lookup = $this->getValueLookup();
- if (isset($lookup[$value])) {
- return $lookup[$value]->name;
- }
-
- if ($value instanceof \BackedEnum) {
- return $value->name;
- }
-
- if ($value instanceof \UnitEnum) {
- return $value->name;
- }
-
- $safeValue = Utils::printSafe($value);
- throw new SerializationError("Cannot serialize value as enum: {$safeValue}");
- }
-
- /**
- * @throws \InvalidArgumentException
- * @throws InvariantViolation
- *
- * @return MixedStore<EnumValueDefinition>
- */
- private function getValueLookup(): MixedStore
- {
- if (! isset($this->valueLookup)) {
- $this->valueLookup = new MixedStore();
-
- foreach ($this->getValues() as $value) {
- $this->valueLookup->offsetSet($value->value, $value);
- }
- }
-
- return $this->valueLookup;
- }
-
- /**
- * @throws Error
- * @throws InvariantViolation
- */
- public function parseValue($value)
- {
- if (! is_string($value)) {
- $safeValue = Utils::printSafeJson($value);
- throw new Error("Enum \"{$this->name}\" cannot represent non-string value: {$safeValue}.{$this->didYouMean($safeValue)}");
- }
-
- if (! isset($this->nameLookup)) {
- $this->initializeNameLookup();
- }
-
- if (! isset($this->nameLookup[$value])) {
- throw new Error("Value \"{$value}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($value)}");
- }
-
- return $this->nameLookup[$value]->value;
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- * @throws InvariantViolation
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null)
- {
- if (! $valueNode instanceof EnumValueNode) {
- $valueStr = Printer::doPrint($valueNode);
- throw new Error("Enum \"{$this->name}\" cannot represent non-enum value: {$valueStr}.{$this->didYouMean($valueStr)}", $valueNode);
- }
-
- $name = $valueNode->value;
-
- if (! isset($this->nameLookup)) {
- $this->initializeNameLookup();
- }
-
- if (isset($this->nameLookup[$name])) {
- return $this->nameLookup[$name]->value;
- }
-
- $valueStr = Printer::doPrint($valueNode);
- throw new Error("Value \"{$valueStr}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($valueStr)}", $valueNode);
- }
-
- /**
- * @throws Error
- * @throws InvariantViolation
- */
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
-
- $values = $this->config['values'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
- if (! is_iterable($values) && ! is_callable($values)) {
- $notIterable = Utils::printSafe($values);
- throw new InvariantViolation("{$this->name} values must be an iterable or callable, got: {$notIterable}");
- }
-
- $this->getValues();
- }
-
- /** @throws InvariantViolation */
- private function initializeNameLookup(): void
- {
- $this->nameLookup = [];
- foreach ($this->getValues() as $value) {
- $this->nameLookup[$value->name] = $value;
- }
- }
-
- /** @throws InvariantViolation */
- protected function didYouMean(string $unknownValue): ?string
- {
- $suggestions = Utils::suggestionList(
- $unknownValue,
- array_map(
- static fn (EnumValueDefinition $value): string => $value->name,
- $this->getValues()
- )
- );
-
- return $suggestions === []
- ? null
- : ' Did you mean the enum value ' . Utils::quotedOrList($suggestions) . '?';
- }
-
- public function astNode(): ?EnumTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<EnumTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php
deleted file mode 100644
index 83516a3f127..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-
-/**
- * @phpstan-type EnumValueConfig array{
- * name: string,
- * value?: mixed,
- * deprecationReason?: string|null,
- * description?: string|null,
- * astNode?: EnumValueDefinitionNode|null
- * }
- */
-class EnumValueDefinition
-{
- public string $name;
-
- /** @var mixed */
- public $value;
-
- public ?string $deprecationReason;
-
- public ?string $description;
-
- public ?EnumValueDefinitionNode $astNode;
-
- /** @phpstan-var EnumValueConfig */
- public array $config;
-
- /** @phpstan-param EnumValueConfig $config */
- public function __construct(array $config)
- {
- $this->name = $config['name'];
- $this->value = $config['value'] ?? null;
- $this->deprecationReason = $config['deprecationReason'] ?? null;
- $this->description = $config['description'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
-
- $this->config = $config;
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->deprecationReason;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php
deleted file mode 100644
index d3170e107c2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php
+++ /dev/null
@@ -1,251 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @see Executor
- *
- * @phpstan-import-type FieldResolver from Executor
- * @phpstan-import-type ArgsMapper from Executor
- * @phpstan-import-type ArgumentListConfig from Argument
- *
- * @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType)
- * @phpstan-type ComplexityFn callable(int, array<string, mixed>): int
- * @phpstan-type VisibilityFn callable(): bool
- * @phpstan-type FieldDefinitionConfig array{
- * name: string,
- * type: FieldType,
- * resolve?: FieldResolver|null,
- * args?: ArgumentListConfig|null,
- * argsMapper?: ArgsMapper|null,
- * description?: string|null,
- * visible?: VisibilityFn|bool,
- * deprecationReason?: string|null,
- * astNode?: FieldDefinitionNode|null,
- * complexity?: ComplexityFn|null
- * }
- * @phpstan-type UnnamedFieldDefinitionConfig array{
- * type: FieldType,
- * resolve?: FieldResolver|null,
- * args?: ArgumentListConfig|null,
- * argsMapper?: ArgsMapper|null,
- * description?: string|null,
- * visible?: VisibilityFn|bool,
- * deprecationReason?: string|null,
- * astNode?: FieldDefinitionNode|null,
- * complexity?: ComplexityFn|null
- * }
- * @phpstan-type FieldsConfig iterable<mixed>|callable(): iterable<mixed>
- */
-/*
- * TODO check if newer versions of PHPStan can handle the full definition, it currently crashes when it is used
- * @phpstan-type EagerListEntry FieldDefinitionConfig|(Type&OutputType)
- * @phpstan-type EagerMapEntry UnnamedFieldDefinitionConfig|FieldDefinition
- * @phpstan-type FieldsList iterable<EagerListEntry|(callable(): EagerListEntry)>
- * @phpstan-type FieldsMap iterable<string, EagerMapEntry|(callable(): EagerMapEntry)>
- * @phpstan-type FieldsIterable FieldsList|FieldsMap
- * @phpstan-type FieldsConfig FieldsIterable|(callable(): FieldsIterable)
- */
-class FieldDefinition
-{
- public string $name;
-
- /** @var array<int, Argument> */
- public array $args;
-
- /**
- * Callback to transform args to value object.
- *
- * @var callable|null
- *
- * @phpstan-var ArgsMapper|null
- */
- public $argsMapper;
-
- /**
- * Callback for resolving field value given parent value.
- *
- * @var callable|null
- *
- * @phpstan-var FieldResolver|null
- */
- public $resolveFn;
-
- public ?string $description;
-
- /**
- * @var callable|bool
- *
- * @phpstan-var VisibilityFn|bool
- */
- public $visible;
-
- public ?string $deprecationReason;
-
- public ?FieldDefinitionNode $astNode;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ComplexityFn|null
- */
- public $complexityFn;
-
- /**
- * Original field definition config.
- *
- * @phpstan-var FieldDefinitionConfig
- */
- public array $config;
-
- /** @var Type&OutputType */
- private Type $type;
-
- /** @param FieldDefinitionConfig $config */
- public function __construct(array $config)
- {
- $this->name = $config['name'];
- $this->resolveFn = $config['resolve'] ?? null;
- $this->args = isset($config['args'])
- ? Argument::listFromConfig($config['args'])
- : [];
- $this->argsMapper = $config['argsMapper'] ?? null;
- $this->description = $config['description'] ?? null;
- $this->visible = $config['visible'] ?? true;
- $this->deprecationReason = $config['deprecationReason'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->complexityFn = $config['complexity'] ?? null;
-
- $this->config = $config;
- }
-
- /**
- * @param ObjectType|InterfaceType $parentType
- * @param callable|iterable $fields
- *
- * @phpstan-param FieldsConfig $fields
- *
- * @throws InvariantViolation
- *
- * @return array<string, self|UnresolvedFieldDefinition>
- */
- public static function defineFieldMap(Type $parentType, $fields): array
- {
- if (is_callable($fields)) {
- $fields = $fields();
- }
-
- if (! is_iterable($fields)) {
- throw new InvariantViolation("{$parentType->name} fields must be an iterable or a callable which returns such an iterable.");
- }
-
- $map = [];
- foreach ($fields as $maybeName => $field) {
- if (is_array($field)) {
- if (! isset($field['name'])) {
- if (! is_string($maybeName)) {
- throw new InvariantViolation("{$parentType->name} fields must be an associative array with field names as keys or a function which returns such an array.");
- }
-
- $field['name'] = $maybeName;
- }
-
- // @phpstan-ignore-next-line PHPStan won't let us define the whole type
- $fieldDef = new self($field);
- } elseif ($field instanceof self) {
- $fieldDef = $field;
- } elseif (is_callable($field)) {
- if (! is_string($maybeName)) {
- throw new InvariantViolation("{$parentType->name} lazy fields must be an associative array with field names as keys.");
- }
-
- $fieldDef = new UnresolvedFieldDefinition($maybeName, $field);
- } elseif ($field instanceof Type) {
- // @phpstan-ignore-next-line PHPStan won't let us define the whole type
- $fieldDef = new self([
- 'name' => $maybeName,
- 'type' => $field,
- ]);
- } else {
- $invalidFieldConfig = Utils::printSafe($field);
- throw new InvariantViolation("{$parentType->name}.{$maybeName} field config must be an array, but got: {$invalidFieldConfig}");
- }
-
- $map[$fieldDef->getName()] = $fieldDef;
- }
-
- return $map;
- }
-
- public function getArg(string $name): ?Argument
- {
- foreach ($this->args as $arg) {
- if ($arg->name === $name) {
- return $arg;
- }
- }
-
- return null;
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- /** @return Type&OutputType */
- public function getType(): Type
- {
- return $this->type ??= Schema::resolveType($this->config['type']);
- }
-
- public function isVisible(): bool
- {
- if (is_bool($this->visible)) {
- return $this->visible;
- }
-
- return $this->visible = ($this->visible)();
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->deprecationReason;
- }
-
- /**
- * @param Type&NamedType $parentType
- *
- * @throws InvariantViolation
- */
- public function assertValid(Type $parentType): void
- {
- $error = Utils::isValidNameError($this->name);
- if ($error !== null) {
- throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}");
- }
-
- $type = Type::getNamedType($this->getType());
-
- if (! $type instanceof OutputType) {
- $safeType = Utils::printSafe($this->type);
- throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Output Type but got: {$safeType}.");
- }
-
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if ($this->resolveFn !== null && ! is_callable($this->resolveFn)) {
- $safeResolveFn = Utils::printSafe($this->resolveFn);
- throw new InvariantViolation("{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: {$safeResolveFn}.");
- }
-
- foreach ($this->args as $fieldArgument) {
- $fieldArgument->assertValid($this, $type);
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php
deleted file mode 100644
index 4dd9cac874a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class FloatType extends ScalarType
-{
- public string $name = Type::FLOAT;
-
- public ?string $description
- = 'The `Float` scalar type represents signed double-precision fractional
-values as specified by
-[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ';
-
- /** @throws SerializationError */
- public function serialize($value): float
- {
- $float = is_numeric($value) || is_bool($value)
- ? (float) $value
- : null;
-
- if ($float === null || ! is_finite($float)) {
- $notFloat = Utils::printSafe($value);
- throw new SerializationError("Float cannot represent non numeric value: {$notFloat}");
- }
-
- return $float;
- }
-
- /** @throws Error */
- public function parseValue($value): float
- {
- $float = is_float($value) || is_int($value)
- ? (float) $value
- : null;
-
- if ($float === null || ! is_finite($float)) {
- $notFloat = Utils::printSafeJson($value);
- throw new Error("Float cannot represent non numeric value: {$notFloat}");
- }
-
- return $float;
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null)
- {
- if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) {
- return (float) $valueNode->value;
- }
-
- $notFloat = Printer::doPrint($valueNode);
- throw new Error("Float cannot represent non numeric value: {$notFloat}", $valueNode);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php
deleted file mode 100644
index eaba00481a3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-
-interface HasFieldsType
-{
- /** @throws InvariantViolation */
- public function getField(string $name): FieldDefinition;
-
- public function hasField(string $name): bool;
-
- public function findField(string $name): ?FieldDefinition;
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, FieldDefinition>
- */
- public function getFields(): array;
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, FieldDefinition>
- */
- public function getVisibleFields(): array;
-
- /**
- * Get all field names, including only visible fields.
- *
- * @throws InvariantViolation
- *
- * @return array<int, string>
- */
- public function getFieldNames(): array;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php
deleted file mode 100644
index 516e568124e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-
-/**
- * @see HasFieldsType
- */
-trait HasFieldsTypeImplementation
-{
- /**
- * Lazily initialized.
- *
- * @var array<string, FieldDefinition|UnresolvedFieldDefinition>
- */
- private array $fields;
-
- /** @throws InvariantViolation */
- private function initializeFields(): void
- {
- if (isset($this->fields)) {
- return;
- }
-
- $this->fields = FieldDefinition::defineFieldMap($this, $this->config['fields']);
- }
-
- /** @throws InvariantViolation */
- public function getField(string $name): FieldDefinition
- {
- $field = $this->findField($name);
-
- if ($field === null) {
- throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\"");
- }
-
- return $field;
- }
-
- /** @throws InvariantViolation */
- public function findField(string $name): ?FieldDefinition
- {
- $this->initializeFields();
-
- if (! isset($this->fields[$name])) {
- return null;
- }
-
- $field = $this->fields[$name];
- if ($field instanceof UnresolvedFieldDefinition) {
- return $this->fields[$name] = $field->resolve();
- }
-
- return $field;
- }
-
- /** @throws InvariantViolation */
- public function hasField(string $name): bool
- {
- $this->initializeFields();
-
- return isset($this->fields[$name]);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, FieldDefinition>
- */
- public function getFields(): array
- {
- $this->initializeFields();
-
- foreach ($this->fields as $name => $field) {
- if ($field instanceof UnresolvedFieldDefinition) {
- $this->fields[$name] = $field->resolve();
- }
- }
-
- // @phpstan-ignore-next-line all field definitions are now resolved
- return $this->fields;
- }
-
- /** @return array<string, FieldDefinition> */
- public function getVisibleFields(): array
- {
- return array_filter(
- $this->getFields(),
- fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible()
- );
- }
-
- /** @throws InvariantViolation */
- public function getFieldNames(): array
- {
- $this->initializeFields();
-
- $visibleFieldNames = array_map(
- fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(),
- $this->getVisibleFields()
- );
-
- return array_values($visibleFieldNames);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php
deleted file mode 100644
index 1d923ad305c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class IDType extends ScalarType
-{
- public string $name = 'ID';
-
- public ?string $description
- = 'The `ID` scalar type represents a unique identifier, often used to
-refetch an object or as key for a cache. The ID type appears in a JSON
-response as a String; however, it is not intended to be human-readable.
-When expected as an input type, any string (such as `"4"`) or integer
-(such as `4`) input value will be accepted as an ID.';
-
- /** @throws SerializationError */
- public function serialize($value): string
- {
- $canCast = is_string($value)
- || is_int($value)
- || (is_object($value) && method_exists($value, '__toString'));
-
- if (! $canCast) {
- $notID = Utils::printSafe($value);
- throw new SerializationError("ID cannot represent a non-string and non-integer value: {$notID}");
- }
-
- return (string) $value;
- }
-
- /** @throws Error */
- public function parseValue($value): string
- {
- if (is_string($value) || is_int($value)) {
- return (string) $value;
- }
-
- $notID = Utils::printSafeJson($value);
- throw new Error("ID cannot represent a non-string and non-integer value: {$notID}");
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null): string
- {
- if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) {
- return $valueNode->value;
- }
-
- $notID = Printer::doPrint($valueNode);
- throw new Error("ID cannot represent a non-string and non-integer value: {$notID}", $valueNode);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php
deleted file mode 100644
index f93f9885fc1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/**
- * export type GraphQLImplementingType =
- * GraphQLObjectType |
- * GraphQLInterfaceType;.
- */
-interface ImplementingType
-{
- public function implementsInterface(InterfaceType $interfaceType): bool;
-
- /** @return array<int, InterfaceType> */
- public function getInterfaces(): array;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php
deleted file mode 100644
index 387ffac926b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * @see ImplementingType
- */
-trait ImplementingTypeImplementation
-{
- /**
- * Lazily initialized.
- *
- * @var array<int, InterfaceType>
- */
- private array $interfaces;
-
- public function implementsInterface(InterfaceType $interfaceType): bool
- {
- if (! isset($this->interfaces)) {
- $this->initializeInterfaces();
- }
-
- foreach ($this->interfaces as $interface) {
- if ($interfaceType->name === $interface->name) {
- return true;
- }
- }
-
- return false;
- }
-
- /** @return array<int, InterfaceType> */
- public function getInterfaces(): array
- {
- if (! isset($this->interfaces)) {
- $this->initializeInterfaces();
- }
-
- return $this->interfaces;
- }
-
- private function initializeInterfaces(): void
- {
- $this->interfaces = [];
-
- if (! isset($this->config['interfaces'])) {
- return;
- }
-
- $interfaces = $this->config['interfaces'];
- if (is_callable($interfaces)) {
- $interfaces = $interfaces();
- }
-
- foreach ($interfaces as $interface) {
- $this->interfaces[] = Schema::resolveType($interface); // @phpstan-ignore argument.templateType
- }
- }
-
- /** @throws InvariantViolation */
- protected function assertValidInterfaces(): void
- {
- if (! isset($this->config['interfaces'])) {
- return;
- }
-
- $interfaces = $this->config['interfaces'];
- if (is_callable($interfaces)) {
- $interfaces = $interfaces();
- }
-
- // @phpstan-ignore-next-line should not happen if used correctly
- if (! is_iterable($interfaces)) {
- throw new InvariantViolation("{$this->name} interfaces must be an iterable or a callable which returns an iterable.");
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php
deleted file mode 100644
index 516f80b05ef..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType)
- * @phpstan-type InputObjectFieldConfig array{
- * name: string,
- * type: ArgumentType,
- * defaultValue?: mixed,
- * description?: string|null,
- * deprecationReason?: string|null,
- * astNode?: InputValueDefinitionNode|null
- * }
- * @phpstan-type UnnamedInputObjectFieldConfig array{
- * name?: string,
- * type: ArgumentType,
- * defaultValue?: mixed,
- * description?: string|null,
- * deprecationReason?: string|null,
- * astNode?: InputValueDefinitionNode|null
- * }
- */
-class InputObjectField
-{
- public string $name;
-
- /** @var mixed */
- public $defaultValue;
-
- public ?string $description;
-
- public ?string $deprecationReason;
-
- /** @var Type&InputType */
- private Type $type;
-
- public ?InputValueDefinitionNode $astNode;
-
- /** @phpstan-var InputObjectFieldConfig */
- public array $config;
-
- /** @phpstan-param InputObjectFieldConfig $config */
- public function __construct(array $config)
- {
- $this->name = $config['name'];
- $this->defaultValue = $config['defaultValue'] ?? null;
- $this->description = $config['description'] ?? null;
- $this->deprecationReason = $config['deprecationReason'] ?? null;
- // Do nothing for type, it is lazy loaded in getType()
- $this->astNode = $config['astNode'] ?? null;
-
- $this->config = $config;
- }
-
- /** @return Type&InputType */
- public function getType(): Type
- {
- if (! isset($this->type)) {
- $this->type = Schema::resolveType($this->config['type']);
- }
-
- return $this->type;
- }
-
- public function defaultValueExists(): bool
- {
- return array_key_exists('defaultValue', $this->config);
- }
-
- public function isRequired(): bool
- {
- return $this->getType() instanceof NonNull
- && ! $this->defaultValueExists();
- }
-
- public function isDeprecated(): bool
- {
- return (bool) $this->deprecationReason;
- }
-
- /**
- * @param Type&NamedType $parentType
- *
- * @throws InvariantViolation
- */
- public function assertValid(Type $parentType): void
- {
- $error = Utils::isValidNameError($this->name);
- if ($error !== null) {
- throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}");
- }
-
- $type = Type::getNamedType($this->getType());
-
- if (! $type instanceof InputType) {
- $notInputType = Utils::printSafe($this->type);
- throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Input Type but got: {$notInputType}");
- }
-
- // @phpstan-ignore-next-line should not happen if used properly
- if (array_key_exists('resolve', $this->config)) {
- throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers.");
- }
-
- if ($this->isRequired() && $this->isDeprecated()) {
- throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated.");
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php
deleted file mode 100644
index af8b70f7f7f..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php
+++ /dev/null
@@ -1,259 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
- *
- * @phpstan-type EagerFieldConfig InputObjectField|(Type&InputType)|UnnamedInputObjectFieldConfig
- * @phpstan-type LazyFieldConfig callable(): EagerFieldConfig
- * @phpstan-type FieldConfig EagerFieldConfig|LazyFieldConfig
- * @phpstan-type ParseValueFn callable(array<string, mixed>): mixed
- * @phpstan-type InputObjectConfig array{
- * name?: string|null,
- * description?: string|null,
- * isOneOf?: bool|null,
- * fields: iterable<FieldConfig>|callable(): iterable<FieldConfig>,
- * parseValue?: ParseValueFn|null,
- * astNode?: InputObjectTypeDefinitionNode|null,
- * extensionASTNodes?: array<InputObjectTypeExtensionNode>|null
- * }
- */
-class InputObjectType extends Type implements InputType, NullableType, NamedType
-{
- use NamedTypeImplementation;
-
- public bool $isOneOf;
-
- /**
- * Lazily initialized.
- *
- * @var array<string, InputObjectField>
- */
- private array $fields;
-
- /** @var ParseValueFn|null */
- private $parseValue;
-
- public ?InputObjectTypeDefinitionNode $astNode;
-
- /** @var array<InputObjectTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /** @phpstan-var InputObjectConfig */
- public array $config;
-
- /**
- * @phpstan-param InputObjectConfig $config
- *
- * @throws InvariantViolation
- * @throws InvariantViolation
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? null;
- $this->isOneOf = $config['isOneOf'] ?? false;
- // $this->fields is initialized lazily
- $this->parseValue = $config['parseValue'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- /** @throws InvariantViolation */
- public function getField(string $name): InputObjectField
- {
- $field = $this->findField($name);
-
- if ($field === null) {
- throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\"");
- }
-
- return $field;
- }
-
- /** @throws InvariantViolation */
- public function findField(string $name): ?InputObjectField
- {
- if (! isset($this->fields)) {
- $this->initializeFields();
- }
-
- return $this->fields[$name] ?? null;
- }
-
- /** @throws InvariantViolation */
- public function hasField(string $name): bool
- {
- if (! isset($this->fields)) {
- $this->initializeFields();
- }
-
- return isset($this->fields[$name]);
- }
-
- /** Returns true if this is a oneOf input object type. */
- public function isOneOf(): bool
- {
- return $this->isOneOf;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, InputObjectField>
- */
- public function getFields(): array
- {
- if (! isset($this->fields)) {
- $this->initializeFields();
- }
-
- return $this->fields;
- }
-
- /** @throws InvariantViolation */
- protected function initializeFields(): void
- {
- $fields = $this->config['fields'];
- if (is_callable($fields)) {
- $fields = $fields();
- }
-
- $this->fields = [];
- foreach ($fields as $nameOrIndex => $field) {
- $this->initializeField($nameOrIndex, $field);
- }
- }
-
- /**
- * @param string|int $nameOrIndex
- *
- * @phpstan-param FieldConfig $field
- *
- * @throws InvariantViolation
- */
- protected function initializeField($nameOrIndex, $field): void
- {
- if (is_callable($field)) {
- $field = $field();
- }
- assert($field instanceof Type || is_array($field) || $field instanceof InputObjectField);
-
- if ($field instanceof Type) {
- $field = ['type' => $field];
- }
- assert(is_array($field) || $field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types
-
- if (is_array($field)) {
- $field['name'] ??= $nameOrIndex;
-
- if (! is_string($field['name'])) {
- throw new InvariantViolation("{$this->name} fields must be an associative array with field names as keys, an array of arrays with a name attribute, or a callable which returns one of those.");
- }
-
- $field = new InputObjectField($field); // @phpstan-ignore-line array type is wrongly inferred
- }
- assert($field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types
-
- $this->fields[$field->name] = $field;
- }
-
- /**
- * Parses an externally provided value (query variable) to use as an input.
- *
- * Should throw an exception with a client-friendly message on invalid values, @see ClientAware.
- *
- * @param array<string, mixed> $value
- *
- * @return mixed
- */
- public function parseValue(array $value)
- {
- if (isset($this->parseValue)) {
- return ($this->parseValue)($value);
- }
-
- return $value;
- }
-
- /**
- * Validates type config and throws if one of the type options is invalid.
- * Note: this method is shallow, it won't validate object fields and their arguments.
- *
- * @throws Error
- * @throws InvariantViolation
- */
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
-
- $fields = $this->config['fields'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
- if (is_callable($fields)) {
- $fields = $fields();
- }
-
- if (! is_iterable($fields)) {
- $invalidFields = Utils::printSafe($fields);
- throw new InvariantViolation("{$this->name} fields must be an iterable or a callable which returns an iterable, got: {$invalidFields}.");
- }
-
- $resolvedFields = $this->getFields();
-
- foreach ($resolvedFields as $field) {
- $field->assertValid($this);
- }
-
- // Additional validation for oneOf input objects
- if ($this->isOneOf()) {
- $this->validateOneOfConstraints($resolvedFields);
- }
- }
-
- /**
- * Validates that oneOf input object constraints are met.
- *
- * @param array<string, InputObjectField> $fields
- *
- * @throws InvariantViolation
- */
- private function validateOneOfConstraints(array $fields): void
- {
- if (count($fields) === 0) {
- throw new InvariantViolation("OneOf input object type {$this->name} must define one or more fields.");
- }
-
- foreach ($fields as $fieldName => $field) {
- $fieldType = $field->getType();
-
- // OneOf fields must be nullable (not wrapped in NonNull)
- if ($fieldType instanceof NonNull) {
- throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} must be nullable.");
- }
-
- // OneOf fields cannot have default values
- if ($field->defaultValueExists()) {
- throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} cannot have a default value.");
- }
- }
- }
-
- public function astNode(): ?InputObjectTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<InputObjectTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php
deleted file mode 100644
index 09d87941cef..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/**
- * export type InputType =
- * | ScalarType
- * | EnumType
- * | InputObjectType
- * | ListOfType<InputType>
- * | NonNull<
- * | ScalarType
- * | EnumType
- * | InputObjectType
- * | ListOfType<InputType>,
- * >;.
- */
-interface InputType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php
deleted file mode 100644
index 4fdd037b8a5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class IntType extends ScalarType
-{
- // As per the Automattic\WooCommerce\Vendor\GraphQL Spec, Integers are only treated as valid when a valid
- // 32-bit signed integer, providing the broadest support across platforms.
- //
- // n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because
- // they are internally represented as IEEE 754 doubles.
- public const MAX_INT = 2147483647;
- public const MIN_INT = -2147483648;
-
- public string $name = Type::INT;
-
- public ?string $description
- = 'The `Int` scalar type represents non-fractional signed whole numeric
-values. Int can represent values between -(2^31) and 2^31 - 1. ';
-
- /** @throws SerializationError */
- public function serialize($value): int
- {
- // Fast path for 90+% of cases:
- if (is_int($value) && $value <= self::MAX_INT && $value >= self::MIN_INT) {
- return $value;
- }
-
- $float = is_numeric($value) || is_bool($value)
- ? (float) $value
- : null;
-
- if ($float === null || floor($float) !== $float) {
- $notInt = Utils::printSafe($value);
- throw new SerializationError("Int cannot represent non-integer value: {$notInt}");
- }
-
- if ($float > self::MAX_INT || $float < self::MIN_INT) {
- $outOfRangeInt = Utils::printSafe($value);
- throw new SerializationError("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}");
- }
-
- return (int) $float;
- }
-
- /** @throws Error */
- public function parseValue($value): int
- {
- $isInt = is_int($value)
- || (is_float($value) && floor($value) === $value);
-
- if (! $isInt) {
- $notInt = Utils::printSafeJson($value);
- throw new Error("Int cannot represent non-integer value: {$notInt}");
- }
-
- if ($value > self::MAX_INT || $value < self::MIN_INT) {
- $outOfRangeInt = Utils::printSafeJson($value);
- throw new Error("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}");
- }
-
- return (int) $value;
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null): int
- {
- if ($valueNode instanceof IntValueNode) {
- $val = (int) $valueNode->value;
- if ($valueNode->value === (string) $val && $val >= self::MIN_INT && $val <= self::MAX_INT) {
- return $val;
- }
- }
-
- $notInt = Printer::doPrint($valueNode);
- throw new Error("Int cannot represent non-integer value: {$notInt}", $valueNode);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php
deleted file mode 100644
index b3263f43e99..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-import-type ResolveType from AbstractType
- * @phpstan-import-type ResolveValue from AbstractType
- * @phpstan-import-type FieldsConfig from FieldDefinition
- *
- * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
- * @phpstan-type InterfaceConfig array{
- * name?: string|null,
- * description?: string|null,
- * fields: FieldsConfig,
- * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
- * resolveType?: ResolveType|null,
- * resolveValue?: ResolveValue|null,
- * astNode?: InterfaceTypeDefinitionNode|null,
- * extensionASTNodes?: array<InterfaceTypeExtensionNode>|null
- * }
- */
-class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType
-{
- use HasFieldsTypeImplementation;
- use NamedTypeImplementation;
- use ImplementingTypeImplementation;
-
- public ?InterfaceTypeDefinitionNode $astNode;
-
- /** @var array<InterfaceTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /** @phpstan-var InterfaceConfig */
- public array $config;
-
- /**
- * @phpstan-param InterfaceConfig $config
- *
- * @throws InvariantViolation
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- /**
- * @param mixed $type
- *
- * @throws InvariantViolation
- */
- public static function assertInterfaceType($type): self
- {
- if (! $type instanceof self) {
- $notInterfaceType = Utils::printSafe($type);
- throw new InvariantViolation("Expected {$notInterfaceType} to be a Automattic\WooCommerce\Vendor\GraphQL Interface type.");
- }
-
- return $type;
- }
-
- public function resolveValue($objectValue, $context, ResolveInfo $info)
- {
- if (isset($this->config['resolveValue'])) {
- return ($this->config['resolveValue'])($objectValue, $context, $info);
- }
-
- return $objectValue;
- }
-
- public function resolveType($objectValue, $context, ResolveInfo $info)
- {
- if (isset($this->config['resolveType'])) {
- return ($this->config['resolveType'])($objectValue, $context, $info);
- }
-
- return null;
- }
-
- /**
- * @throws Error
- * @throws InvariantViolation
- */
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
-
- $resolveType = $this->config['resolveType'] ?? null;
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if ($resolveType !== null && ! is_callable($resolveType)) {
- $notCallable = Utils::printSafe($resolveType);
- throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}.");
- }
-
- $this->assertValidInterfaces();
- }
-
- public function astNode(): ?InterfaceTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<InterfaceTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php
deleted file mode 100644
index 2a897b29beb..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-
-/*
-export type GraphQLLeafType =
-GraphQLScalarType |
-GraphQLEnumType;
-*/
-
-interface LeafType
-{
- /**
- * Serializes an internal value to include in a response.
- *
- * Should throw an exception on invalid values.
- *
- * @param mixed $value
- *
- * @throws SerializationError
- *
- * @return mixed
- */
- public function serialize($value);
-
- /**
- * Parses an externally provided value (query variable) to use as an input.
- *
- * Should throw an exception with a client-friendly message on invalid values, @see ClientAware.
- *
- * @param mixed $value
- *
- * @throws Error
- *
- * @return mixed
- */
- public function parseValue($value);
-
- /**
- * Parses an externally provided literal value (hardcoded in Automattic\WooCommerce\Vendor\GraphQL query) to use as an input.
- *
- * Should throw an exception with a client-friendly message on invalid value nodes, @see ClientAware.
- *
- * @param ValueNode&Node $valueNode
- * @param array<string, mixed>|null $variables
- *
- * @throws Error
- *
- * @return mixed
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null);
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php
deleted file mode 100644
index 95d49d76055..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * @template-covariant OfType of Type
- */
-class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType
-{
- /**
- * @var Type|callable
- *
- * @phpstan-var OfType|callable(): OfType
- */
- private $wrappedType;
-
- /**
- * @param Type|callable $type
- *
- * @phpstan-param OfType|callable(): OfType $type
- */
- public function __construct($type)
- {
- $this->wrappedType = $type;
- }
-
- public function toString(): string
- {
- return '[' . $this->getWrappedType()->toString() . ']';
- }
-
- /** @phpstan-return OfType */
- public function getWrappedType(): Type
- {
- return Schema::resolveType($this->wrappedType);
- }
-
- public function getInnermostType(): NamedType
- {
- $type = $this->getWrappedType();
- while ($type instanceof WrappingType) {
- $type = $type->getWrappedType();
- }
-
- assert($type instanceof NamedType, 'known because we unwrapped all the way down');
-
- return $type;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php
deleted file mode 100644
index 85c9b1cd8a8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-
-/**
- * export type NamedType =
- * | ScalarType
- * | ObjectType
- * | InterfaceType
- * | UnionType
- * | EnumType
- * | InputObjectType;.
- *
- * @property string $name
- * @property string|null $description
- * @property (Node&TypeDefinitionNode)|null $astNode
- * @property array<Node&TypeExtensionNode> $extensionASTNodes
- */
-interface NamedType
-{
- /** @throws Error */
- public function assertValid(): void;
-
- /** Is this type a built-in type? */
- public function isBuiltInType(): bool;
-
- public function name(): string;
-
- public function description(): ?string;
-
- /** @return (Node&TypeDefinitionNode)|null */
- public function astNode(): ?Node;
-
- /** @return array<Node&TypeExtensionNode> */
- public function extensionASTNodes(): array;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php
deleted file mode 100644
index 55f0005e8e3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-
-/**
- * @see NamedType
- */
-trait NamedTypeImplementation
-{
- public string $name;
-
- public ?string $description;
-
- public function toString(): string
- {
- return $this->name;
- }
-
- /** @throws InvariantViolation */
- protected function inferName(): string
- {
- if (isset($this->name)) { // @phpstan-ignore-line property might be uninitialized
- return $this->name;
- }
-
- // If class is extended - infer name from className
- // QueryType -> Type
- // SomeOtherType -> SomeOther
- $reflection = new \ReflectionClass($this);
- $name = $reflection->getShortName();
-
- if ($reflection->getNamespaceName() !== __NAMESPACE__) {
- $withoutPrefixType = preg_replace('~Type$~', '', $name);
- assert(is_string($withoutPrefixType), 'regex is statically known to be correct');
-
- return $withoutPrefixType;
- }
-
- throw new InvariantViolation('Must provide name for Type.');
- }
-
- public function isBuiltInType(): bool
- {
- return in_array($this->name, Type::BUILT_IN_TYPE_NAMES, true);
- }
-
- public function name(): string
- {
- return $this->name;
- }
-
- public function description(): ?string
- {
- return $this->description;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php
deleted file mode 100644
index a2bffdcc931..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * @phpstan-type WrappedType (NullableType&Type)|callable():(NullableType&Type)
- */
-class NonNull extends Type implements WrappingType, OutputType, InputType
-{
- /**
- * @var Type|callable
- *
- * @phpstan-var WrappedType
- */
- private $wrappedType;
-
- /**
- * @param Type|callable $type
- *
- * @phpstan-param WrappedType $type
- */
- public function __construct($type)
- {
- $this->wrappedType = $type;
- }
-
- public function toString(): string
- {
- return $this->getWrappedType()->toString() . '!';
- }
-
- /** @return NullableType&Type */
- public function getWrappedType(): Type
- {
- return Schema::resolveType($this->wrappedType);
- }
-
- public function getInnermostType(): NamedType
- {
- $type = $this->getWrappedType();
- while ($type instanceof WrappingType) {
- $type = $type->getWrappedType();
- }
-
- assert($type instanceof NamedType, 'known because we unwrapped all the way down');
-
- return $type;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php
deleted file mode 100644
index 241667b2ef8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/*
-export type GraphQLNullableType =
- | GraphQLScalarType
- | GraphQLObjectType
- | GraphQLInterfaceType
- | GraphQLUnionType
- | GraphQLEnumType
- | GraphQLInputObjectType
- | GraphQLList<any>;
- */
-
-interface NullableType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php
deleted file mode 100644
index 796e4cf53fc..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php
+++ /dev/null
@@ -1,176 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Object Type Definition.
- *
- * Most Automattic\WooCommerce\Vendor\GraphQL types you define will be object types.
- * Object types have a name, but most importantly describe their fields.
- *
- * Example:
- *
- * $AddressType = new ObjectType([
- * 'name' => 'Address',
- * 'fields' => [
- * 'street' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
- * 'number' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::int(),
- * 'formatted' => [
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
- * 'resolve' => fn (AddressModel $address): string => "{$address->number} {$address->street}",
- * ],
- * ],
- * ]);
- *
- * When two types need to refer to each other, or a type needs to refer to
- * itself in a field, you can use a function expression (aka a closure or a
- * thunk) to supply the fields lazily.
- *
- * Example:
- *
- * $PersonType = null;
- * $PersonType = new ObjectType([
- * 'name' => 'Person',
- * 'fields' => fn (): array => [
- * 'name' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
- * 'bestFriend' => $PersonType,
- * ],
- * ]);
- *
- * @phpstan-import-type FieldResolver from Executor
- * @phpstan-import-type ArgsMapper from Executor
- *
- * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
- * @phpstan-type ObjectConfig array{
- * name?: string|null,
- * description?: string|null,
- * resolveField?: FieldResolver|null,
- * argsMapper?: ArgsMapper|null,
- * fields: (callable(): iterable<mixed>)|iterable<mixed>,
- * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
- * isTypeOf?: (callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): (bool|Deferred|null))|null,
- * astNode?: ObjectTypeDefinitionNode|null,
- * extensionASTNodes?: array<ObjectTypeExtensionNode>|null
- * }
- */
-class ObjectType extends Type implements OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType
-{
- use HasFieldsTypeImplementation;
- use NamedTypeImplementation;
- use ImplementingTypeImplementation;
-
- public ?ObjectTypeDefinitionNode $astNode;
-
- /** @var array<ObjectTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /**
- * @var callable|null
- *
- * @phpstan-var FieldResolver|null
- */
- public $resolveFieldFn;
-
- /**
- * @var callable|null
- *
- * @phpstan-var ArgsMapper|null
- */
- public $argsMapper;
-
- /** @phpstan-var ObjectConfig */
- public array $config;
-
- /**
- * @phpstan-param ObjectConfig $config
- *
- * @throws InvariantViolation
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? null;
- $this->resolveFieldFn = $config['resolveField'] ?? null;
- $this->argsMapper = $config['argsMapper'] ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- /**
- * @param mixed $type
- *
- * @throws InvariantViolation
- */
- public static function assertObjectType($type): self
- {
- if (! $type instanceof self) {
- $notObjectType = Utils::printSafe($type);
- throw new InvariantViolation("Expected {$notObjectType} to be a Automattic\WooCommerce\Vendor\GraphQL Object type.");
- }
-
- return $type;
- }
-
- /**
- * @param mixed $objectValue The resolved value for the object type
- * @param mixed $context The context that was passed to GraphQL::execute()
- *
- * @return bool|Deferred|null
- */
- public function isTypeOf($objectValue, $context, ResolveInfo $info)
- {
- return isset($this->config['isTypeOf'])
- ? $this->config['isTypeOf'](
- $objectValue,
- $context,
- $info
- )
- : null;
- }
-
- /**
- * Validates type config and throws if one of the type options is invalid.
- * Note: this method is shallow, it won't validate object fields and their arguments.
- *
- * @throws Error
- * @throws InvariantViolation
- */
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
-
- $isTypeOf = $this->config['isTypeOf'] ?? null;
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if (isset($isTypeOf) && ! is_callable($isTypeOf)) {
- $notCallable = Utils::printSafe($isTypeOf);
- throw new InvariantViolation("{$this->name} must provide \"isTypeOf\" as null or a callable, but got: {$notCallable}.");
- }
-
- foreach ($this->getFields() as $field) {
- $field->assertValid($this);
- }
-
- $this->assertValidInterfaces();
- }
-
- public function astNode(): ?ObjectTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<ObjectTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php
deleted file mode 100644
index 6664d5e39ff..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/*
-GraphQLScalarType |
-GraphQLObjectType |
-GraphQLInterfaceType |
-GraphQLUnionType |
-GraphQLEnumType |
-GraphQLList |
-GraphQLNonNull;
-*/
-
-interface OutputType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php
deleted file mode 100644
index 9249ad3d317..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\PhpDoc;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-import-type PartialEnumValueConfig from EnumType
- */
-class PhpEnumType extends EnumType
-{
- public const MULTIPLE_DESCRIPTIONS_DISALLOWED = 'Using more than 1 Description attribute is not supported.';
- public const MULTIPLE_DEPRECATIONS_DISALLOWED = 'Using more than 1 Deprecated attribute is not supported.';
-
- /** @var class-string<\UnitEnum> */
- protected string $enumClass;
-
- /**
- * @param class-string<\UnitEnum> $enumClass The fully qualified class name of a native PHP enum
- * @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class
- * @param string|null $description The description the enum will have in the schema, defaults to PHPDoc of the given class
- * @param array<EnumTypeExtensionNode>|null $extensionASTNodes
- *
- * @throws \Exception
- * @throws \ReflectionException
- */
- public function __construct(
- string $enumClass,
- ?string $name = null,
- ?string $description = null,
- ?EnumTypeDefinitionNode $astNode = null,
- ?array $extensionASTNodes = null
- ) {
- $this->enumClass = $enumClass;
- $reflection = new \ReflectionEnum($enumClass);
-
- /**
- * @var array<string, PartialEnumValueConfig> $enumDefinitions
- */
- $enumDefinitions = [];
- foreach ($reflection->getCases() as $case) {
- $enumDefinitions[$case->name] = [
- 'value' => $case->getValue(),
- 'description' => $this->extractDescription($case),
- 'deprecationReason' => $this->deprecationReason($case),
- ];
- }
-
- parent::__construct([
- 'name' => $name ?? $this->baseName($enumClass),
- 'values' => $enumDefinitions,
- 'description' => $description ?? $this->extractDescription($reflection),
- 'astNode' => $astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- public function serialize($value): string
- {
- if ($value instanceof $this->enumClass) {
- return $value->name;
- }
-
- if (is_a($this->enumClass, \BackedEnum::class, true)) {
- try {
- $instance = $this->enumClass::from($value);
- } catch (\ValueError|\TypeError $error) {
- $notEnumInstanceOrValue = Utils::printSafe($value);
- throw new SerializationError("Cannot serialize value as enum: {$notEnumInstanceOrValue}, expected instance or valid value of {$this->enumClass}.", $error->getCode(), $error);
- }
-
- return $instance->name;
- }
-
- $notEnum = Utils::printSafe($value);
- throw new SerializationError("Cannot serialize value as enum: {$notEnum}, expected instance of {$this->enumClass}.");
- }
-
- public function parseValue($value)
- {
- // Can happen when variable values undergo a serialization cycle before execution
- if ($value instanceof $this->enumClass) {
- return $value;
- }
-
- return parent::parseValue($value);
- }
-
- /** @param class-string $class */
- protected function baseName(string $class): string
- {
- $parts = explode('\\', $class);
-
- return end($parts);
- }
-
- /**
- * @param \ReflectionClassConstant|\ReflectionClass<\UnitEnum> $reflection
- *
- * @throws \Exception
- */
- protected function extractDescription(\ReflectionClassConstant|\ReflectionClass $reflection): ?string
- {
- $attributes = $reflection->getAttributes(Description::class);
-
- if (count($attributes) === 1) {
- return $attributes[0]->newInstance()->description;
- }
-
- if (count($attributes) > 1) {
- throw new \Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED);
- }
-
- $comment = $reflection->getDocComment();
- $unpadded = PhpDoc::unpad($comment);
-
- return PhpDoc::unwrap($unpadded);
- }
-
- /** @throws \Exception */
- protected function deprecationReason(\ReflectionClassConstant $reflection): ?string
- {
- $attributes = $reflection->getAttributes(Deprecated::class);
-
- if (count($attributes) === 1) {
- return $attributes[0]->newInstance()->reason;
- }
-
- if (count($attributes) > 1) {
- throw new \Exception(self::MULTIPLE_DEPRECATIONS_DISALLOWED);
- }
-
- return null;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php
deleted file mode 100644
index 0083348811c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php
+++ /dev/null
@@ -1,307 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * @phpstan-type QueryPlanOptions array{
- * groupImplementorFields?: bool,
- * }
- */
-class QueryPlan
-{
- /**
- * Map from type names to a list of fields referenced of that type.
- *
- * @var array<string, array<string, true>>
- */
- private array $typeToFields = [];
-
- private Schema $schema;
-
- /** @var array<string, mixed> */
- private array $queryPlan = [];
-
- /** @var array<string, mixed> */
- private array $variableValues;
-
- /** @var array<string, FragmentDefinitionNode> */
- private array $fragments;
-
- private bool $groupImplementorFields;
-
- /**
- * @param iterable<FieldNode> $fieldNodes
- * @param array<string, mixed> $variableValues
- * @param array<string, FragmentDefinitionNode> $fragments
- * @param QueryPlanOptions $options
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- */
- public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = [])
- {
- $this->schema = $schema;
- $this->variableValues = $variableValues;
- $this->fragments = $fragments;
- $this->groupImplementorFields = $options['groupImplementorFields'] ?? false;
- $this->analyzeQueryPlan($parentType, $fieldNodes);
- }
-
- /** @return array<string, mixed> */
- public function queryPlan(): array
- {
- return $this->queryPlan;
- }
-
- /** @return array<int, string> */
- public function getReferencedTypes(): array
- {
- return array_keys($this->typeToFields);
- }
-
- public function hasType(string $type): bool
- {
- return isset($this->typeToFields[$type]);
- }
-
- /**
- * TODO return array<string, true>.
- *
- * @return array<int, string>
- */
- public function getReferencedFields(): array
- {
- $allFields = [];
- foreach ($this->typeToFields as $fields) {
- foreach ($fields as $field => $_) {
- $allFields[$field] = true;
- }
- }
-
- return array_keys($allFields);
- }
-
- public function hasField(string $field): bool
- {
- foreach ($this->typeToFields as $fields) {
- if (array_key_exists($field, $fields)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * TODO return array<string, true>.
- *
- * @return array<int, string>
- */
- public function subFields(string $typename): array
- {
- return array_keys($this->typeToFields[$typename] ?? []);
- }
-
- /**
- * @param iterable<FieldNode> $fieldNodes
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- */
- private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes): void
- {
- $queryPlan = [];
- $implementors = [];
- foreach ($fieldNodes as $fieldNode) {
- if ($fieldNode->selectionSet === null) {
- continue;
- }
-
- $type = Type::getNamedType(
- $parentType->getField($fieldNode->name->value)->getType()
- );
-
- $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors);
- $queryPlan = $this->arrayMergeDeep($queryPlan, $subfields);
- }
-
- if ($this->groupImplementorFields) {
- $this->queryPlan = ['fields' => $queryPlan];
-
- if ($implementors !== []) {
- $this->queryPlan['implementors'] = $implementors;
- }
- } else {
- $this->queryPlan = $queryPlan;
- }
- }
-
- /**
- * @param Type&NamedType $parentType
- * @param array<string, mixed> $implementors
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<mixed>
- */
- private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors): array
- {
- $fields = [];
- $implementors = [];
- foreach ($selectionSet->selections as $selection) {
- if ($selection instanceof FieldNode) {
- $fieldName = $selection->name->value;
-
- if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
- continue;
- }
-
- assert($parentType instanceof HasFieldsType, 'ensured by query validation');
-
- $type = $parentType->getField($fieldName);
- $selectionType = $type->getType();
-
- $subImplementors = [];
- $nestedSelectionSet = $selection->selectionSet;
- $subfields = $nestedSelectionSet === null
- ? []
- : $this->analyzeSubFields($selectionType, $nestedSelectionSet, $subImplementors);
-
- $fields[$fieldName] = [
- 'type' => $selectionType,
- 'fields' => $subfields,
- 'args' => Values::getArgumentValues($type, $selection, $this->variableValues),
- ];
- if ($this->groupImplementorFields && $subImplementors !== []) {
- $fields[$fieldName]['implementors'] = $subImplementors;
- }
- } elseif ($selection instanceof FragmentSpreadNode) {
- $spreadName = $selection->name->value;
- $fragment = $this->fragments[$spreadName] ?? null;
- if ($fragment === null) {
- continue;
- }
-
- $type = $this->schema->getType($fragment->typeCondition->name->value);
- assert($type instanceof Type, 'ensured by query validation');
-
- $subfields = $this->analyzeSubFields($type, $fragment->selectionSet);
- $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
- } elseif ($selection instanceof InlineFragmentNode) {
- $typeCondition = $selection->typeCondition;
- $type = $typeCondition === null
- ? $parentType
- : $this->schema->getType($typeCondition->name->value);
- assert($type instanceof Type, 'ensured by query validation');
-
- $subfields = $this->analyzeSubFields($type, $selection->selectionSet);
- $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
- }
- }
-
- $parentTypeName = $parentType->name();
-
- // TODO evaluate if this line is really necessary.
- // It causes abstract types to appear in getReferencedTypes() even if they do not have any fields directly referencing them.
- $this->typeToFields[$parentTypeName] ??= [];
- foreach ($fields as $fieldName => $_) {
- $this->typeToFields[$parentTypeName][$fieldName] = true;
- }
-
- return $fields;
- }
-
- /**
- * @param array<string, mixed> $implementors
- *
- * @throws \Exception
- * @throws Error
- *
- * @return array<mixed>
- */
- private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet, array &$implementors = []): array
- {
- $type = Type::getNamedType($type);
-
- return $type instanceof ObjectType || $type instanceof AbstractType
- ? $this->analyzeSelectionSet($selectionSet, $type, $implementors)
- : [];
- }
-
- /**
- * @param Type&NamedType $parentType
- * @param Type&NamedType $type
- * @param array<mixed> $fields
- * @param array<mixed> $subfields
- * @param array<string, mixed> $implementors
- *
- * @return array<mixed>
- */
- private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors): array
- {
- if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) {
- $name = $type->name;
- assert(is_string($name));
-
- $implementors[$name] = [
- 'type' => $type,
- 'fields' => $this->arrayMergeDeep(
- $implementors[$name]['fields'] ?? [],
- array_diff_key($subfields, $fields)
- ),
- ];
-
- $fields = $this->arrayMergeDeep(
- $fields,
- array_intersect_key($subfields, $fields)
- );
- } else {
- $fields = $this->arrayMergeDeep($subfields, $fields);
- }
-
- return $fields;
- }
-
- /**
- * Merges nested arrays, but handles non array values differently from array_merge_recursive.
- * While array_merge_recursive tries to merge non-array values, in this implementation they will be overwritten.
- *
- * @see https://stackoverflow.com/a/25712428
- *
- * @param array<mixed> $array1
- * @param array<mixed> $array2
- *
- * @return array<mixed>
- */
- private function arrayMergeDeep(array $array1, array $array2): array
- {
- foreach ($array2 as $key => &$value) {
- if (is_numeric($key)) {
- if (! in_array($value, $array1, true)) {
- $array1[] = $value;
- }
- } elseif (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
- $array1[$key] = $this->arrayMergeDeep($array1[$key], $value);
- } else {
- $array1[$key] = $value;
- }
- }
-
- return $array1;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php
deleted file mode 100644
index 4591b6b26db..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php
+++ /dev/null
@@ -1,512 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * Structure containing information useful for field resolution process.
- *
- * Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md).
- *
- * @phpstan-import-type QueryPlanOptions from QueryPlan
- *
- * @phpstan-type Path list<string|int>
- */
-class ResolveInfo
-{
- /**
- * The definition of the field being resolved.
- *
- * @api
- */
- public FieldDefinition $fieldDefinition;
-
- /**
- * The name of the field being resolved.
- *
- * @api
- */
- public string $fieldName;
-
- /**
- * Expected return type of the field being resolved.
- *
- * @api
- */
- public Type $returnType;
-
- /**
- * AST of all nodes referencing this field in the query.
- *
- * @api
- *
- * @var \ArrayObject<int, FieldNode>
- */
- public \ArrayObject $fieldNodes;
-
- /**
- * Parent type of the field being resolved.
- *
- * @api
- */
- public ObjectType $parentType;
-
- /**
- * Path to this field from the very root value. When fields are aliased, the path includes aliases.
- *
- * @api
- *
- * @var list<string|int>
- *
- * @phpstan-var Path
- */
- public array $path;
-
- /**
- * Path to this field from the very root value. This will never include aliases.
- *
- * @api
- *
- * @var list<string|int>
- *
- * @phpstan-var Path
- */
- public array $unaliasedPath;
-
- /**
- * Instance of a schema used for execution.
- *
- * @api
- */
- public Schema $schema;
-
- /**
- * AST of all fragments defined in query.
- *
- * @api
- *
- * @var array<string, FragmentDefinitionNode>
- */
- public array $fragments = [];
-
- /**
- * Root value passed to query execution.
- *
- * @api
- *
- * @var mixed
- */
- public $rootValue;
-
- /**
- * AST of operation definition node (query, mutation).
- *
- * @api
- */
- public OperationDefinitionNode $operation;
-
- /**
- * Array of variables passed to query execution.
- *
- * @api
- *
- * @var array<string, mixed>
- */
- public array $variableValues = [];
-
- /**
- * @param \ArrayObject<int, FieldNode> $fieldNodes
- * @param list<string|int> $path
- * @param array<string, FragmentDefinitionNode> $fragments
- * @param mixed|null $rootValue
- * @param array<string, mixed> $variableValues
- * @param list<string|int> $unaliasedPath
- *
- * @phpstan-param Path $path
- * @phpstan-param Path $unaliasedPath
- */
- public function __construct(
- FieldDefinition $fieldDefinition,
- \ArrayObject $fieldNodes,
- ObjectType $parentType,
- array $path,
- Schema $schema,
- array $fragments,
- $rootValue,
- OperationDefinitionNode $operation,
- array $variableValues,
- array $unaliasedPath = []
- ) {
- $this->fieldDefinition = $fieldDefinition;
- $this->fieldName = $fieldDefinition->name;
- $this->returnType = $fieldDefinition->getType();
- $this->fieldNodes = $fieldNodes;
- $this->parentType = $parentType;
- $this->path = $path;
- $this->unaliasedPath = $unaliasedPath;
- $this->schema = $schema;
- $this->fragments = $fragments;
- $this->rootValue = $rootValue;
- $this->operation = $operation;
- $this->variableValues = $variableValues;
- }
-
- /**
- * Returns names of all fields selected in query for `$this->fieldName` up to `$depth` levels.
- *
- * Example:
- * {
- * root {
- * id
- * nested {
- * nested1
- * nested2 {
- * nested3
- * }
- * }
- * }
- * }
- *
- * Given this ResolveInfo instance is a part of root field resolution, and $depth === 1,
- * this method will return:
- * [
- * 'id' => true,
- * 'nested' => [
- * 'nested1' => true,
- * 'nested2' => true,
- * ],
- * ]
- *
- * This method does not consider conditional typed fragments.
- * Use it with care for fields of interface and union types.
- *
- * @param int $depth How many levels to include in the output beyond the first
- *
- * @return array<string, mixed>
- *
- * @api
- */
- public function getFieldSelection(int $depth = 0): array
- {
- $fields = [];
-
- foreach ($this->fieldNodes as $fieldNode) {
- $selectionSet = $fieldNode->selectionSet;
- if ($selectionSet !== null) {
- $fields = array_merge_recursive(
- $fields,
- $this->foldSelectionSet($selectionSet, $depth)
- );
- }
- }
-
- return $fields;
- }
-
- /**
- * Returns names and args of all fields selected in query for `$this->fieldName` up to `$depth` levels, including aliases.
- *
- * The result maps original field names to a map of selections for that field, including aliases.
- * For each of those selections, you can find the following keys:
- * - "args" contains the passed arguments for this field/alias (not on an union inline fragment)
- * - "type" contains the related Type instance found (will be the same for all aliases of a field)
- * - "selectionSet" contains potential nested fields of this field/alias (only on ObjectType). The structure is recursive from here.
- * - "unions" contains potential object types contained in an UnionType (only on UnionType). The structure is recursive from here and will go through the selectionSet of the object types.
- *
- * Example:
- * {
- * root {
- * id
- * nested {
- * nested1(myArg: 1)
- * nested1Bis: nested1
- * }
- * alias1: nested {
- * nested1(myArg: 2, mySecondAg: "test")
- * }
- * myUnion(myArg: 3) {
- * ...on Nested {
- * nested1(myArg: 4)
- * }
- * ...on MyCustomObject {
- * nested3
- * }
- * }
- * }
- * }
- *
- * Given this ResolveInfo instance is a part of root field resolution,
- * $depth === 1,
- * and fields "nested" represents an ObjectType named "Nested",
- * this method will return:
- * [
- * 'id' => [
- * 'id' => [
- * 'args' => [],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IntType Object ( ... )),
- * ],
- * ],
- * 'nested' => [
- * 'nested' => [
- * 'args' => [],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
- * 'selectionSet' => [
- * 'nested1' => [
- * 'nested1' => [
- * 'args' => [
- * 'myArg' => 1,
- * ],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
- * ],
- * 'nested1Bis' => [
- * 'args' => [],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
- * ],
- * ],
- * ],
- * ],
- * ],
- * 'alias1' => [
- * 'alias1' => [
- * 'args' => [],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
- * 'selectionSet' => [
- * 'nested1' => [
- * 'nested1' => [
- * 'args' => [
- * 'myArg' => 2,
- * 'mySecondAg' => "test",
- * ],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
- * ],
- * ],
- * ],
- * ],
- * ],
- * 'myUnion' => [
- * 'myUnion' => [
- * 'args' => [
- * 'myArg' => 3,
- * ],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType Object ( ... )),
- * 'unions' => [
- * 'Nested' => [
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
- * 'selectionSet' => [
- * 'nested1' => [
- * 'nested1' => [
- * 'args' => [
- * 'myArg' => 4,
- * ],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
- * ],
- * ],
- * ],
- * ],
- * 'MyCustomObject' => [
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\TestClasses\MyCustomType Object ( ... )),
- * 'selectionSet' => [
- * 'nested3' => [
- * 'nested3' => [
- * 'args' => [],
- * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
- * ],
- * ],
- * ],
- * ],
- * ],
- * ],
- * ],
- * ]
- *
- * @param int $depth How many levels to include in the output beyond the first
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<string, mixed>
- *
- * @api
- */
- public function getFieldSelectionWithAliases(int $depth = 0): array
- {
- $fields = [];
-
- foreach ($this->fieldNodes as $fieldNode) {
- $selectionSet = $fieldNode->selectionSet;
- if ($selectionSet !== null) {
- $field = $this->parentType->getField($fieldNode->name->value);
- $fieldType = $field->getType();
-
- $fields = array_merge_recursive(
- $fields,
- $this->foldSelectionWithAlias($selectionSet, $depth, $fieldType)
- );
- }
- }
-
- return $fields;
- }
-
- /**
- * @param QueryPlanOptions $options
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- */
- public function lookAhead(array $options = []): QueryPlan
- {
- return new QueryPlan(
- $this->parentType,
- $this->schema,
- $this->fieldNodes,
- $this->variableValues,
- $this->fragments,
- $options
- );
- }
-
- /** @return array<string, bool> */
- private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array
- {
- /** @var array<string, bool> $fields */
- $fields = [];
-
- foreach ($selectionSet->selections as $selection) {
- if ($selection instanceof FieldNode) {
- $fields[$selection->name->value] = $descend > 0 && $selection->selectionSet !== null
- ? array_merge_recursive(
- $fields[$selection->name->value] ?? [],
- $this->foldSelectionSet($selection->selectionSet, $descend - 1)
- )
- : true;
- } elseif ($selection instanceof FragmentSpreadNode) {
- $spreadName = $selection->name->value;
- $fragment = $this->fragments[$spreadName] ?? null;
- if ($fragment === null) {
- continue;
- }
-
- $fields = array_merge_recursive(
- $this->foldSelectionSet($fragment->selectionSet, $descend),
- $fields
- );
- } elseif ($selection instanceof InlineFragmentNode) {
- $fields = array_merge_recursive(
- $this->foldSelectionSet($selection->selectionSet, $descend),
- $fields
- );
- }
- }
-
- return $fields;
- }
-
- /**
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<string>
- */
- private function foldSelectionWithAlias(SelectionSetNode $selectionSet, int $descend, Type $parentType): array
- {
- /** @var array<string, bool> $fields */
- $fields = [];
-
- if ($parentType instanceof WrappingType) {
- $parentType = $parentType->getInnermostType();
- }
-
- foreach ($selectionSet->selections as $selection) {
- if ($selection instanceof FieldNode) {
- $fieldName = $selection->name->value;
- $aliasName = $selection->alias->value ?? $fieldName;
-
- if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
- continue;
- }
- assert($parentType instanceof HasFieldsType, 'ensured by query validation');
-
- $aliasInfo = &$fields[$fieldName][$aliasName];
-
- $fieldDef = $parentType->getField($fieldName);
-
- $aliasInfo['args'] = Values::getArgumentValues($fieldDef, $selection, $this->variableValues);
-
- $fieldType = $fieldDef->getType();
-
- $namedFieldType = $fieldType;
- if ($namedFieldType instanceof WrappingType) {
- $namedFieldType = $namedFieldType->getInnermostType();
- }
-
- $aliasInfo['type'] = $namedFieldType;
-
- if ($descend <= 0) {
- continue;
- }
-
- $nestedSelectionSet = $selection->selectionSet;
- if ($nestedSelectionSet === null) {
- continue;
- }
-
- if ($namedFieldType instanceof UnionType) {
- $aliasInfo['unions'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend, $fieldType);
- continue;
- }
-
- $aliasInfo['selectionSet'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend - 1, $fieldType);
- } elseif ($selection instanceof FragmentSpreadNode) {
- $spreadName = $selection->name->value;
- $fragment = $this->fragments[$spreadName] ?? null;
- if ($fragment === null) {
- continue;
- }
-
- $fieldType = $this->schema->getType($fragment->typeCondition->name->value);
- assert($fieldType instanceof Type, 'ensured by query validation');
-
- $fields = array_merge_recursive(
- $this->foldSelectionWithAlias($fragment->selectionSet, $descend, $fieldType),
- $fields
- );
- } elseif ($selection instanceof InlineFragmentNode) {
- $typeCondition = $selection->typeCondition;
- $fieldType = $typeCondition === null
- ? $parentType
- : $this->schema->getType($typeCondition->name->value);
- assert($fieldType instanceof Type, 'ensured by query validation');
-
- if ($parentType instanceof UnionType) {
- assert($fieldType instanceof NamedType, 'ensured by query validation');
- $fieldTypeInfo = &$fields[$fieldType->name()];
- $fieldTypeInfo['type'] = $fieldType;
- $fieldTypeInfo['selectionSet'] = $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType);
- continue;
- }
-
- $fields = array_merge_recursive(
- $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType),
- $fields
- );
- }
- }
-
- return $fields;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php
deleted file mode 100644
index ac40a26b1b7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Scalar Type Definition.
- *
- * The leaf values of any request and input values to arguments are
- * Scalars (or Enums) and are defined with a name and a series of coercion
- * functions used to ensure validity.
- *
- * Example:
- *
- * class OddType extends ScalarType
- * {
- * public $name = 'Odd',
- * public function serialize($value)
- * {
- * return $value % 2 === 1 ? $value : null;
- * }
- * }
- *
- * @phpstan-type ScalarConfig array{
- * name?: string|null,
- * description?: string|null,
- * astNode?: ScalarTypeDefinitionNode|null,
- * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
- * }
- */
-abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NullableType, NamedType
-{
- use NamedTypeImplementation;
-
- public ?ScalarTypeDefinitionNode $astNode;
-
- /** @var array<ScalarTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /** @phpstan-var ScalarConfig */
- public array $config;
-
- /**
- * @phpstan-param ScalarConfig $config
- *
- * @throws InvariantViolation
- */
- public function __construct(array $config = [])
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? $this->description ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
- }
-
- public function astNode(): ?ScalarTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<ScalarTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php
deleted file mode 100644
index 22c9835a5b3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class StringType extends ScalarType
-{
- public string $name = Type::STRING;
-
- public ?string $description
- = 'The `String` scalar type represents textual data, represented as UTF-8
-character sequences. The String type is most often used by Automattic\WooCommerce\Vendor\GraphQL to
-represent free-form human-readable text.';
-
- /** @throws SerializationError */
- public function serialize($value): string
- {
- $canCast = is_scalar($value)
- || (is_object($value) && method_exists($value, '__toString'))
- || $value === null;
-
- if (! $canCast) {
- $notStringable = Utils::printSafe($value);
- throw new SerializationError("String cannot represent value: {$notStringable}");
- }
-
- return (string) $value;
- }
-
- /** @throws Error */
- public function parseValue($value): string
- {
- if (! is_string($value)) {
- $notString = Utils::printSafeJson($value);
- throw new Error("String cannot represent a non string value: {$notString}");
- }
-
- return $value;
- }
-
- /**
- * @throws \JsonException
- * @throws Error
- */
- public function parseLiteral(Node $valueNode, ?array $variables = null): string
- {
- if ($valueNode instanceof StringValueNode) {
- return $valueNode->value;
- }
-
- $notString = Printer::doPrint($valueNode);
- throw new Error("String cannot represent a non string value: {$notString}", $valueNode);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php
deleted file mode 100644
index d4892631266..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php
+++ /dev/null
@@ -1,350 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Registry of built-in Automattic\WooCommerce\Vendor\GraphQL types and base class for all other types.
- */
-abstract class Type implements \JsonSerializable
-{
- public const INT = 'Int';
- public const FLOAT = 'Float';
- public const STRING = 'String';
- public const BOOLEAN = 'Boolean';
- public const ID = 'ID';
-
- /** @var list<string> */
- public const BUILT_IN_SCALAR_NAMES = [
- self::INT,
- self::FLOAT,
- self::STRING,
- self::BOOLEAN,
- self::ID,
- ];
-
- /**
- * @deprecated use {@see Type::BUILT_IN_SCALAR_NAMES}
- *
- * @var list<string>
- */
- public const STANDARD_TYPE_NAMES = self::BUILT_IN_SCALAR_NAMES;
-
- /**
- * Names of all built-in types: built-in scalars and introspection types.
- *
- * @see Type::BUILT_IN_SCALAR_NAMES for just the built-in scalar names.
- *
- * @var list<string>
- */
- public const BUILT_IN_TYPE_NAMES = [
- ...self::BUILT_IN_SCALAR_NAMES,
- ...Introspection::TYPE_NAMES,
- ];
-
- /** @var array<string, ScalarType>|null */
- protected static ?array $builtInScalars;
-
- /** @var array<string, Type&NamedType>|null */
- protected static ?array $builtInTypes;
-
- /**
- * Returns the built-in Int scalar type.
- *
- * @api
- */
- public static function int(): ScalarType
- {
- return static::$builtInScalars[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- }
-
- /**
- * Returns the built-in Float scalar type.
- *
- * @api
- */
- public static function float(): ScalarType
- {
- return static::$builtInScalars[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- }
-
- /**
- * Returns the built-in String scalar type.
- *
- * @api
- */
- public static function string(): ScalarType
- {
- return static::$builtInScalars[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- }
-
- /**
- * Returns the built-in Boolean scalar type.
- *
- * @api
- */
- public static function boolean(): ScalarType
- {
- return static::$builtInScalars[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- }
-
- /**
- * Returns the built-in ID scalar type.
- *
- * @api
- */
- public static function id(): ScalarType
- {
- return static::$builtInScalars[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- }
-
- /**
- * Wraps the given type in a list type.
- *
- * @template T of Type
- *
- * @param T|callable():T $type
- *
- * @return ListOfType<T>
- *
- * @api
- */
- public static function listOf($type): ListOfType
- {
- return new ListOfType($type);
- }
-
- /**
- * Wraps the given type in a non-null type.
- *
- * @param NonNull|(NullableType&Type)|callable():(NullableType&Type) $type
- *
- * @api
- */
- public static function nonNull($type): NonNull
- {
- if ($type instanceof NonNull) {
- return $type;
- }
-
- return new NonNull($type);
- }
-
- /**
- * Returns all built-in types: built-in scalars and introspection types.
- *
- * @api
- *
- * @return array<string, Type&NamedType>
- */
- public static function builtInTypes(): array
- {
- return self::$builtInTypes ??= array_merge(
- Introspection::getTypes(),
- self::builtInScalars()
- );
- }
-
- /**
- * Returns all built-in scalar types.
- *
- * @api
- *
- * @return array<string, ScalarType>
- */
- public static function builtInScalars(): array
- {
- return [
- self::INT => static::int(),
- self::FLOAT => static::float(),
- self::STRING => static::string(),
- self::BOOLEAN => static::boolean(),
- self::ID => static::id(),
- ];
- }
-
- /**
- * Returns all built-in scalar types.
- *
- * @deprecated use {@see Type::builtInScalars()}
- *
- * @return array<string, ScalarType>
- */
- public static function getStandardTypes(): array
- {
- return self::builtInScalars();
- }
-
- /**
- * Allows partially or completely overriding the standard types globally.
- *
- * @deprecated prefer per-schema scalar overrides via {@see SchemaConfig::$types} or {@see SchemaConfig::$typeLoader}
- *
- * @param array<ScalarType> $types
- *
- * @throws InvariantViolation
- */
- public static function overrideStandardTypes(array $types): void
- {
- // Reset caches that might contain instances of built-in scalars
- static::$builtInTypes = null;
- Introspection::resetCachedInstances();
- Directive::resetCachedInstances();
-
- foreach ($types as $type) {
- // @phpstan-ignore-next-line generic type is not enforced by PHP
- if (! $type instanceof ScalarType) {
- $typeClass = ScalarType::class;
- $notType = Utils::printSafe($type);
- throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}");
- }
-
- if (! self::isBuiltInScalarName($type->name)) {
- $standardTypeNames = implode(', ', self::BUILT_IN_SCALAR_NAMES);
- $notStandardTypeName = Utils::printSafe($type->name);
- throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}");
- }
-
- static::$builtInScalars[$type->name] = $type;
- }
- }
-
- /**
- * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID).
- *
- * Does not unwrap NonNull/List wrappers — checks the type instance directly.
- * ScalarType is a NamedType, so {@see Type::getNamedType()} is unnecessary.
- *
- * @param mixed $type
- *
- * @phpstan-assert-if-true ScalarType $type
- *
- * @api
- */
- public static function isBuiltInScalar($type): bool
- {
- return $type instanceof ScalarType
- && self::isBuiltInScalarName($type->name);
- }
-
- /** Checks if the given name is one of the built-in scalar type names (ID, String, Int, Float, Boolean). */
- public static function isBuiltInScalarName(string $name): bool
- {
- return in_array($name, self::BUILT_IN_SCALAR_NAMES, true);
- }
-
- /**
- * Determines if the given type is an input type.
- *
- * @param mixed $type
- *
- * @api
- */
- public static function isInputType($type): bool
- {
- return self::getNamedType($type) instanceof InputType;
- }
-
- /**
- * Returns the underlying named type of the given type.
- *
- * @return (Type&NamedType)|null
- *
- * @phpstan-return ($type is null ? null : Type&NamedType)
- *
- * @api
- */
- public static function getNamedType(?Type $type): ?Type
- {
- if ($type instanceof WrappingType) {
- return $type->getInnermostType();
- }
-
- assert($type === null || $type instanceof NamedType, 'only other option');
-
- return $type;
- }
-
- /**
- * Determines if the given type is an output type.
- *
- * @param mixed $type
- *
- * @api
- */
- public static function isOutputType($type): bool
- {
- return self::getNamedType($type) instanceof OutputType;
- }
-
- /**
- * Determines if the given type is a leaf type.
- *
- * @param mixed $type
- *
- * @api
- */
- public static function isLeafType($type): bool
- {
- return $type instanceof LeafType;
- }
-
- /**
- * Determines if the given type is a composite type.
- *
- * @param mixed $type
- *
- * @api
- */
- public static function isCompositeType($type): bool
- {
- return $type instanceof CompositeType;
- }
-
- /**
- * Determines if the given type is an abstract type.
- *
- * @param mixed $type
- *
- * @api
- */
- public static function isAbstractType($type): bool
- {
- return $type instanceof AbstractType;
- }
-
- /**
- * Unwraps a potentially non-null type to return the underlying nullable type.
- *
- * @return Type&NullableType
- *
- * @api
- */
- public static function getNullableType(Type $type): Type
- {
- if ($type instanceof NonNull) {
- return $type->getWrappedType();
- }
-
- assert($type instanceof NullableType, 'only other option');
-
- return $type;
- }
-
- abstract public function toString(): string;
-
- public function __toString(): string
- {
- return $this->toString();
- }
-
- #[\ReturnTypeWillChange]
- public function jsonSerialize(): string
- {
- return $this->toString();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php
deleted file mode 100644
index 76838906692..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-import-type ResolveType from AbstractType
- * @phpstan-import-type ResolveValue from AbstractType
- *
- * @phpstan-type ObjectTypeReference ObjectType|callable(): ObjectType
- * @phpstan-type UnionConfig array{
- * name?: string|null,
- * description?: string|null,
- * types: iterable<ObjectTypeReference>|callable(): iterable<ObjectTypeReference>,
- * resolveType?: ResolveType|null,
- * resolveValue?: ResolveValue|null,
- * astNode?: UnionTypeDefinitionNode|null,
- * extensionASTNodes?: array<UnionTypeExtensionNode>|null
- * }
- */
-class UnionType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType
-{
- use NamedTypeImplementation;
-
- public ?UnionTypeDefinitionNode $astNode;
-
- /** @var array<UnionTypeExtensionNode> */
- public array $extensionASTNodes;
-
- /** @phpstan-var UnionConfig */
- public array $config;
-
- /**
- * Lazily initialized.
- *
- * @var array<int, ObjectType>
- */
- private array $types;
-
- /**
- * Lazily initialized.
- *
- * @var array<string, bool>
- */
- private array $possibleTypeNames;
-
- /**
- * @phpstan-param UnionConfig $config
- *
- * @throws InvariantViolation
- */
- public function __construct(array $config)
- {
- $this->name = $config['name'] ?? $this->inferName();
- $this->description = $config['description'] ?? $this->description ?? null;
- $this->astNode = $config['astNode'] ?? null;
- $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
-
- $this->config = $config;
- }
-
- /** @throws InvariantViolation */
- public function isPossibleType(Type $type): bool
- {
- if (! $type instanceof ObjectType) {
- return false;
- }
-
- if (! isset($this->possibleTypeNames)) {
- $this->possibleTypeNames = [];
- foreach ($this->getTypes() as $possibleType) {
- $this->possibleTypeNames[$possibleType->name] = true;
- }
- }
-
- return isset($this->possibleTypeNames[$type->name]);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, ObjectType>
- */
- public function getTypes(): array
- {
- if (! isset($this->types)) {
- $this->types = [];
-
- $types = $this->config['types'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
- if (is_callable($types)) {
- $types = $types();
- }
-
- if (! is_iterable($types)) {
- throw new InvariantViolation("Must provide iterable of types or a callable which returns such an iterable for Union {$this->name}.");
- }
-
- foreach ($types as $type) {
- $this->types[] = Schema::resolveType($type); // @phpstan-ignore argument.templateType
- }
- }
-
- return $this->types;
- }
-
- public function resolveValue($objectValue, $context, ResolveInfo $info)
- {
- if (isset($this->config['resolveValue'])) {
- return ($this->config['resolveValue'])($objectValue, $context, $info);
- }
-
- return $objectValue;
- }
-
- public function resolveType($objectValue, $context, ResolveInfo $info)
- {
- if (isset($this->config['resolveType'])) {
- return ($this->config['resolveType'])($objectValue, $context, $info);
- }
-
- return null;
- }
-
- public function assertValid(): void
- {
- Utils::assertValidName($this->name);
-
- $resolveType = $this->config['resolveType'] ?? null;
- // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
- if (isset($resolveType) && ! is_callable($resolveType)) {
- $notCallable = Utils::printSafe($resolveType);
- throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}.");
- }
- }
-
- public function astNode(): ?UnionTypeDefinitionNode
- {
- return $this->astNode;
- }
-
- /** @return array<UnionTypeExtensionNode> */
- public function extensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php
deleted file mode 100644
index ae503758000..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/*
-export type GraphQLUnmodifiedType =
-GraphQLScalarType |
-GraphQLObjectType |
-GraphQLInterfaceType |
-GraphQLUnionType |
-GraphQLEnumType |
-GraphQLInputObjectType;
-*/
-
-interface UnmodifiedType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php
deleted file mode 100644
index bcaa0dcc4cc..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-/**
- * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
- *
- * @phpstan-type DefinitionResolver callable(): (FieldDefinition|(Type&OutputType)|UnnamedFieldDefinitionConfig)
- */
-class UnresolvedFieldDefinition
-{
- private string $name;
-
- /**
- * @var callable
- *
- * @phpstan-var DefinitionResolver
- */
- private $definitionResolver;
-
- /** @param DefinitionResolver $definitionResolver */
- public function __construct(string $name, callable $definitionResolver)
- {
- $this->name = $name;
- $this->definitionResolver = $definitionResolver;
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function resolve(): FieldDefinition
- {
- $field = ($this->definitionResolver)();
-
- if ($field instanceof FieldDefinition) {
- return $field;
- }
-
- if ($field instanceof Type) {
- return new FieldDefinition([
- 'name' => $this->name,
- 'type' => $field,
- ]);
- }
-
- return new FieldDefinition($field + ['name' => $this->name]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php
deleted file mode 100644
index da0b8cec42d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
-
-interface WrappingType
-{
- /** Return the wrapped type, which may itself be a wrapping type. */
- public function getWrappedType(): Type;
-
- /**
- * Return the innermost wrapped type, which is guaranteed to be a named type.
- *
- * @return Type&NamedType
- */
- public function getInnermostType(): NamedType;
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php
deleted file mode 100644
index 7d1c05cd59a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php
+++ /dev/null
@@ -1,834 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\WrappingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * @phpstan-type IntrospectionOptions array{
- * descriptions?: bool,
- * directiveIsRepeatable?: bool,
- * schemaDescription?: bool,
- * typeIsOneOf?: bool,
- * }
- *
- * Available options:
- * - descriptions
- * Include descriptions in the introspection result?
- * Default: true
- * - directiveIsRepeatable
- * Include field `isRepeatable` for directives?
- * Default: false
- * - typeIsOneOf
- * Include field `isOneOf` for types?
- * Default: false
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\IntrospectionTest
- */
-class Introspection
-{
- public const SCHEMA_FIELD_NAME = '__schema';
- public const TYPE_FIELD_NAME = '__type';
- public const TYPE_NAME_FIELD_NAME = '__typename';
-
- public const SCHEMA_OBJECT_NAME = '__Schema';
- public const TYPE_OBJECT_NAME = '__Type';
- public const DIRECTIVE_OBJECT_NAME = '__Directive';
- public const FIELD_OBJECT_NAME = '__Field';
- public const INPUT_VALUE_OBJECT_NAME = '__InputValue';
- public const ENUM_VALUE_OBJECT_NAME = '__EnumValue';
- public const TYPE_KIND_ENUM_NAME = '__TypeKind';
- public const DIRECTIVE_LOCATION_ENUM_NAME = '__DirectiveLocation';
-
- public const TYPE_NAMES = [
- self::SCHEMA_OBJECT_NAME,
- self::TYPE_OBJECT_NAME,
- self::DIRECTIVE_OBJECT_NAME,
- self::FIELD_OBJECT_NAME,
- self::INPUT_VALUE_OBJECT_NAME,
- self::ENUM_VALUE_OBJECT_NAME,
- self::TYPE_KIND_ENUM_NAME,
- self::DIRECTIVE_LOCATION_ENUM_NAME,
- ];
-
- /** @var array<string, mixed>|null */
- protected static ?array $cachedInstances;
-
- /**
- * @param IntrospectionOptions $options
- *
- * @api
- */
- public static function getIntrospectionQuery(array $options = []): string
- {
- $optionsWithDefaults = array_merge([
- 'descriptions' => true,
- 'directiveIsRepeatable' => false,
- 'schemaDescription' => false,
- 'typeIsOneOf' => false,
- ], $options);
-
- $descriptions = $optionsWithDefaults['descriptions']
- ? 'description'
- : '';
- $directiveIsRepeatable = $optionsWithDefaults['directiveIsRepeatable']
- ? 'isRepeatable'
- : '';
- $schemaDescription = $optionsWithDefaults['schemaDescription']
- ? $descriptions
- : '';
- $typeIsOneOf = $optionsWithDefaults['typeIsOneOf']
- ? 'isOneOf'
- : '';
-
- return <<<GRAPHQL
- query IntrospectionQuery {
- __schema {
- {$schemaDescription}
- queryType { name }
- mutationType { name }
- subscriptionType { name }
- types {
- ...FullType
- }
- directives {
- name
- {$descriptions}
- args(includeDeprecated: true) {
- ...InputValue
- }
- {$directiveIsRepeatable}
- locations
- }
- }
- }
-
- fragment FullType on __Type {
- kind
- name
- {$descriptions}
- {$typeIsOneOf}
- fields(includeDeprecated: true) {
- name
- {$descriptions}
- args(includeDeprecated: true) {
- ...InputValue
- }
- type {
- ...TypeRef
- }
- isDeprecated
- deprecationReason
- }
- inputFields(includeDeprecated: true) {
- ...InputValue
- }
- interfaces {
- ...TypeRef
- }
- enumValues(includeDeprecated: true) {
- name
- {$descriptions}
- isDeprecated
- deprecationReason
- }
- possibleTypes {
- ...TypeRef
- }
- }
-
- fragment InputValue on __InputValue {
- name
- {$descriptions}
- type { ...TypeRef }
- defaultValue
- isDeprecated
- deprecationReason
- }
-
- fragment TypeRef on __Type {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- ofType {
- kind
- name
- }
- }
- }
- }
- }
- }
- }
- }
-GRAPHQL;
- }
-
- /**
- * Build an introspection query from a Schema.
- *
- * Introspection is useful for utilities that care about type and field
- * relationships, but do not need to traverse through those relationships.
- *
- * This is the inverse of BuildClientSchema::build(). The primary use case is
- * outside the server context, for instance when doing schema comparisons.
- *
- * @param IntrospectionOptions $options
- *
- * @throws \Exception
- * @throws \JsonException
- * @throws InvariantViolation
- *
- * @return array<string, array<mixed>>
- *
- * @api
- */
- public static function fromSchema(Schema $schema, array $options = []): array
- {
- $optionsWithDefaults = array_merge([
- 'directiveIsRepeatable' => true,
- 'schemaDescription' => true,
- 'typeIsOneOf' => true,
- ], $options);
-
- $result = GraphQL::executeQuery(
- $schema,
- self::getIntrospectionQuery($optionsWithDefaults)
- );
-
- $data = $result->data;
- if ($data === null) {
- $noDataResult = Utils::printSafeJson($result);
- throw new InvariantViolation("Introspection query returned no data: {$noDataResult}.");
- }
-
- return $data;
- }
-
- /** @param Type&NamedType $type */
- public static function isIntrospectionType(NamedType $type): bool
- {
- return in_array($type->name, self::TYPE_NAMES, true);
- }
-
- /** @return array<string, Type&NamedType> */
- public static function getTypes(): array
- {
- return [
- self::SCHEMA_OBJECT_NAME => self::_schema(),
- self::TYPE_OBJECT_NAME => self::_type(),
- self::DIRECTIVE_OBJECT_NAME => self::_directive(),
- self::FIELD_OBJECT_NAME => self::_field(),
- self::INPUT_VALUE_OBJECT_NAME => self::_inputValue(),
- self::ENUM_VALUE_OBJECT_NAME => self::_enumValue(),
- self::TYPE_KIND_ENUM_NAME => self::_typeKind(),
- self::DIRECTIVE_LOCATION_ENUM_NAME => self::_directiveLocation(),
- ];
- }
-
- public static function _schema(): ObjectType
- {
- return self::$cachedInstances[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::SCHEMA_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'A Automattic\WooCommerce\Vendor\GraphQL Schema defines the capabilities of a Automattic\WooCommerce\Vendor\GraphQL '
- . 'server. It exposes all available types and directives on '
- . 'the server, as well as the entry points for query, mutation, and '
- . 'subscription operations.',
- 'fields' => [
- 'description' => [
- 'type' => Type::string(),
- 'resolve' => static fn (Schema $schema): ?string => $schema->description,
- ],
- 'types' => [
- 'description' => 'A list of all types supported by this server.',
- 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))),
- 'resolve' => static fn (Schema $schema): array => $schema->getTypeMap(),
- ],
- 'queryType' => [
- 'description' => 'The type that query operations will be rooted at.',
- 'type' => new NonNull(self::_type()),
- 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getQueryType(),
- ],
- 'mutationType' => [
- 'description' => 'If this server supports mutation, the type that mutation operations will be rooted at.',
- 'type' => self::_type(),
- 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getMutationType(),
- ],
- 'subscriptionType' => [
- 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.',
- 'type' => self::_type(),
- 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getSubscriptionType(),
- ],
- 'directives' => [
- 'description' => 'A list of all directives supported by this server.',
- 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))),
- 'resolve' => static fn (Schema $schema): array => $schema->getDirectives(),
- ],
- ],
- ]);
- }
-
- public static function _type(): ObjectType
- {
- return self::$cachedInstances[self::TYPE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::TYPE_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'The fundamental unit of any Automattic\WooCommerce\Vendor\GraphQL Schema is the type. There are '
- . 'many kinds of types in Automattic\WooCommerce\Vendor\GraphQL as represented by the `__TypeKind` enum.'
- . "\n\n"
- . 'Depending on the kind of a type, certain fields describe '
- . 'information about that type. Scalar types provide no information '
- . 'beyond a name and description, while Enum types provide their values. '
- . 'Object and Interface types provide the fields they describe. Abstract '
- . 'types, Union and Interface, provide the Object types possible '
- . 'at runtime. List and NonNull types compose other types.',
- 'fields' => static fn (): array => [
- 'kind' => [
- 'type' => Type::nonNull(self::_typeKind()),
- 'resolve' => static function (Type $type): string {
- switch (true) {
- case $type instanceof ListOfType:
- return TypeKind::LIST;
- case $type instanceof NonNull:
- return TypeKind::NON_NULL;
- case $type instanceof ScalarType:
- return TypeKind::SCALAR;
- case $type instanceof ObjectType:
- return TypeKind::OBJECT;
- case $type instanceof EnumType:
- return TypeKind::ENUM;
- case $type instanceof InputObjectType:
- return TypeKind::INPUT_OBJECT;
- case $type instanceof InterfaceType:
- return TypeKind::INTERFACE;
- case $type instanceof UnionType:
- return TypeKind::UNION;
- default:
- $safeType = Utils::printSafe($type);
- throw new \Exception("Unknown kind of type: {$safeType}");
- }
- },
- ],
- 'name' => [
- 'type' => Type::string(),
- 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType
- ? $type->name
- : null,
- ],
- 'description' => [
- 'type' => Type::string(),
- 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType
- ? $type->description
- : null,
- ],
- 'fields' => [
- 'type' => Type::listOf(Type::nonNull(self::_field())),
- 'args' => [
- 'includeDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'defaultValue' => false,
- ],
- ],
- 'resolve' => static function (Type $type, $args): ?array {
- if ($type instanceof ObjectType || $type instanceof InterfaceType) {
- $fields = $type->getVisibleFields();
-
- if (! $args['includeDeprecated']) {
- return array_filter(
- $fields,
- static fn (FieldDefinition $field): bool => ! $field->isDeprecated()
- );
- }
-
- return $fields;
- }
-
- return null;
- },
- ],
- 'interfaces' => [
- 'type' => Type::listOf(Type::nonNull(self::_type())),
- 'resolve' => static fn ($type): ?array => $type instanceof ObjectType || $type instanceof InterfaceType
- ? $type->getInterfaces()
- : null,
- ],
- 'possibleTypes' => [
- 'type' => Type::listOf(Type::nonNull(self::_type())),
- 'resolve' => static fn ($type, $args, $context, ResolveInfo $info): ?array => $type instanceof InterfaceType || $type instanceof UnionType
- ? $info->schema->getPossibleTypes($type)
- : null,
- ],
- 'enumValues' => [
- 'type' => Type::listOf(Type::nonNull(self::_enumValue())),
- 'args' => [
- 'includeDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'defaultValue' => false,
- ],
- ],
- 'resolve' => static function ($type, $args): ?array {
- if ($type instanceof EnumType) {
- $values = $type->getValues();
-
- if (! $args['includeDeprecated']) {
- return array_filter(
- $values,
- static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated()
- );
- }
-
- return $values;
- }
-
- return null;
- },
- ],
- 'inputFields' => [
- 'type' => Type::listOf(Type::nonNull(self::_inputValue())),
- 'args' => [
- 'includeDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'defaultValue' => false,
- ],
- ],
- 'resolve' => static function ($type, $args): ?array {
- if ($type instanceof InputObjectType) {
- $fields = $type->getFields();
-
- if (! $args['includeDeprecated']) {
- return array_filter(
- $fields,
- static fn (InputObjectField $field): bool => ! $field->isDeprecated(),
- );
- }
-
- return $fields;
- }
-
- return null;
- },
- ],
- 'ofType' => [
- 'type' => self::_type(),
- 'resolve' => static fn ($type): ?Type => $type instanceof WrappingType
- ? $type->getWrappedType()
- : null,
- ],
- 'isOneOf' => [
- 'type' => Type::boolean(),
- 'resolve' => static fn ($type): ?bool => $type instanceof InputObjectType
- ? $type->isOneOf()
- : null,
- ],
- ],
- ]);
- }
-
- public static function _typeKind(): EnumType
- {
- return self::$cachedInstances[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::TYPE_KIND_ENUM_NAME,
- 'isIntrospection' => true,
- 'description' => 'An enum describing what kind of type a given `__Type` is.',
- 'values' => [
- 'SCALAR' => [
- 'value' => TypeKind::SCALAR,
- 'description' => 'Indicates this type is a scalar.',
- ],
- 'OBJECT' => [
- 'value' => TypeKind::OBJECT,
- 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.',
- ],
- 'INTERFACE' => [
- 'value' => TypeKind::INTERFACE,
- 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.',
- ],
- 'UNION' => [
- 'value' => TypeKind::UNION,
- 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.',
- ],
- 'ENUM' => [
- 'value' => TypeKind::ENUM,
- 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.',
- ],
- 'INPUT_OBJECT' => [
- 'value' => TypeKind::INPUT_OBJECT,
- 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.',
- ],
- 'LIST' => [
- 'value' => TypeKind::LIST,
- 'description' => 'Indicates this type is a list. `ofType` is a valid field.',
- ],
- 'NON_NULL' => [
- 'value' => TypeKind::NON_NULL,
- 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.',
- ],
- ],
- ]);
- }
-
- public static function _field(): ObjectType
- {
- return self::$cachedInstances[self::FIELD_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::FIELD_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'Object and Interface types are described by a list of Fields, each of '
- . 'which has a name, potentially a list of arguments, and a return type.',
- 'fields' => static fn (): array => [
- 'name' => [
- 'type' => Type::nonNull(Type::string()),
- 'resolve' => static fn (FieldDefinition $field): string => $field->name,
- ],
- 'description' => [
- 'type' => Type::string(),
- 'resolve' => static fn (FieldDefinition $field): ?string => $field->description,
- ],
- 'args' => [
- 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
- 'args' => [
- 'includeDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'defaultValue' => false,
- ],
- ],
- 'resolve' => static function (FieldDefinition $field, $args): array {
- $values = $field->args;
-
- if (! $args['includeDeprecated']) {
- return array_filter(
- $values,
- static fn (Argument $value): bool => ! $value->isDeprecated(),
- );
- }
-
- return $values;
- },
- ],
- 'type' => [
- 'type' => Type::nonNull(self::_type()),
- 'resolve' => static fn (FieldDefinition $field): Type => $field->getType(),
- ],
- 'isDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(),
- ],
- 'deprecationReason' => [
- 'type' => Type::string(),
- 'resolve' => static fn (FieldDefinition $field): ?string => $field->deprecationReason,
- ],
- ],
- ]);
- }
-
- public static function _inputValue(): ObjectType
- {
- return self::$cachedInstances[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::INPUT_VALUE_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'Arguments provided to Fields or Directives and the input fields of an '
- . 'InputObject are represented as Input Values which describe their type '
- . 'and optionally a default value.',
- 'fields' => static fn (): array => [
- 'name' => [
- 'type' => Type::nonNull(Type::string()),
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): string => $inputValue->name,
- ],
- 'description' => [
- 'type' => Type::string(),
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): ?string => $inputValue->description,
- ],
- 'type' => [
- 'type' => Type::nonNull(self::_type()),
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): Type => $inputValue->getType(),
- ],
- 'defaultValue' => [
- 'type' => Type::string(),
- 'description' => 'A GraphQL-formatted string representing the default value for this input value.',
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static function ($inputValue): ?string {
- if ($inputValue->defaultValueExists()) {
- $defaultValueAST = AST::astFromValue($inputValue->defaultValue, $inputValue->getType());
-
- if ($defaultValueAST === null) {
- $inconvertibleDefaultValue = Utils::printSafe($inputValue->defaultValue);
- throw new InvariantViolation("Unable to convert defaultValue of argument {$inputValue->name} into AST: {$inconvertibleDefaultValue}.");
- }
-
- return Printer::doPrint($defaultValueAST);
- }
-
- return null;
- },
- ],
- 'isDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(),
- ],
- 'deprecationReason' => [
- 'type' => Type::string(),
- /** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason,
- ],
- ],
- ]);
- }
-
- public static function _enumValue(): ObjectType
- {
- return self::$cachedInstances[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::ENUM_VALUE_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'One possible value for a given Enum. Enum values are unique values, not '
- . 'a placeholder for a string or numeric value. However an Enum value is '
- . 'returned in a JSON response as a string.',
- 'fields' => [
- 'name' => [
- 'type' => Type::nonNull(Type::string()),
- 'resolve' => static fn (EnumValueDefinition $enumValue): string => $enumValue->name,
- ],
- 'description' => [
- 'type' => Type::string(),
- 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->description,
- ],
- 'isDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(),
- ],
- 'deprecationReason' => [
- 'type' => Type::string(),
- 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->deprecationReason,
- ],
- ],
- ]);
- }
-
- public static function _directive(): ObjectType
- {
- return self::$cachedInstances[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::DIRECTIVE_OBJECT_NAME,
- 'isIntrospection' => true,
- 'description' => 'A Directive provides a way to describe alternate runtime execution and '
- . 'type validation behavior in a Automattic\WooCommerce\Vendor\GraphQL document.'
- . "\n\nIn some cases, you need to provide options to alter GraphQL's "
- . 'execution behavior in ways field arguments will not suffice, such as '
- . 'conditionally including or skipping a field. Directives provide this by '
- . 'describing additional information to the executor.',
- 'fields' => [
- 'name' => [
- 'type' => Type::nonNull(Type::string()),
- 'resolve' => static fn (Directive $directive): string => $directive->name,
- ],
- 'description' => [
- 'type' => Type::string(),
- 'resolve' => static fn (Directive $directive): ?string => $directive->description,
- ],
- 'isRepeatable' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'resolve' => static fn (Directive $directive): bool => $directive->isRepeatable,
- ],
- 'locations' => [
- 'type' => Type::nonNull(Type::listOf(Type::nonNull(
- self::_directiveLocation()
- ))),
- 'resolve' => static fn (Directive $directive): array => $directive->locations,
- ],
- 'args' => [
- 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
- 'args' => [
- 'includeDeprecated' => [
- 'type' => Type::nonNull(Type::boolean()),
- 'defaultValue' => false,
- ],
- ],
- 'resolve' => static function (Directive $directive, $args): array {
- $values = $directive->args;
-
- if (! $args['includeDeprecated']) {
- return array_filter(
- $values,
- static fn (Argument $value): bool => ! $value->isDeprecated(),
- );
- }
-
- return $values;
- },
- ],
- ],
- ]);
- }
-
- public static function _directiveLocation(): EnumType
- {
- return self::$cachedInstances[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
- 'name' => self::DIRECTIVE_LOCATION_ENUM_NAME,
- 'isIntrospection' => true,
- 'description' => 'A Directive can be adjacent to many parts of the Automattic\WooCommerce\Vendor\GraphQL language, a '
- . '__DirectiveLocation describes one such possible adjacencies.',
- 'values' => [
- 'QUERY' => [
- 'value' => DirectiveLocation::QUERY,
- 'description' => 'Location adjacent to a query operation.',
- ],
- 'MUTATION' => [
- 'value' => DirectiveLocation::MUTATION,
- 'description' => 'Location adjacent to a mutation operation.',
- ],
- 'SUBSCRIPTION' => [
- 'value' => DirectiveLocation::SUBSCRIPTION,
- 'description' => 'Location adjacent to a subscription operation.',
- ],
- 'FIELD' => [
- 'value' => DirectiveLocation::FIELD,
- 'description' => 'Location adjacent to a field.',
- ],
- 'FRAGMENT_DEFINITION' => [
- 'value' => DirectiveLocation::FRAGMENT_DEFINITION,
- 'description' => 'Location adjacent to a fragment definition.',
- ],
- 'FRAGMENT_SPREAD' => [
- 'value' => DirectiveLocation::FRAGMENT_SPREAD,
- 'description' => 'Location adjacent to a fragment spread.',
- ],
- 'INLINE_FRAGMENT' => [
- 'value' => DirectiveLocation::INLINE_FRAGMENT,
- 'description' => 'Location adjacent to an inline fragment.',
- ],
- 'VARIABLE_DEFINITION' => [
- 'value' => DirectiveLocation::VARIABLE_DEFINITION,
- 'description' => 'Location adjacent to a variable definition.',
- ],
- 'SCHEMA' => [
- 'value' => DirectiveLocation::SCHEMA,
- 'description' => 'Location adjacent to a schema definition.',
- ],
- 'SCALAR' => [
- 'value' => DirectiveLocation::SCALAR,
- 'description' => 'Location adjacent to a scalar definition.',
- ],
- 'OBJECT' => [
- 'value' => DirectiveLocation::OBJECT,
- 'description' => 'Location adjacent to an object type definition.',
- ],
- 'FIELD_DEFINITION' => [
- 'value' => DirectiveLocation::FIELD_DEFINITION,
- 'description' => 'Location adjacent to a field definition.',
- ],
- 'ARGUMENT_DEFINITION' => [
- 'value' => DirectiveLocation::ARGUMENT_DEFINITION,
- 'description' => 'Location adjacent to an argument definition.',
- ],
- 'INTERFACE' => [
- 'value' => DirectiveLocation::IFACE,
- 'description' => 'Location adjacent to an interface definition.',
- ],
- 'UNION' => [
- 'value' => DirectiveLocation::UNION,
- 'description' => 'Location adjacent to a union definition.',
- ],
- 'ENUM' => [
- 'value' => DirectiveLocation::ENUM,
- 'description' => 'Location adjacent to an enum definition.',
- ],
- 'ENUM_VALUE' => [
- 'value' => DirectiveLocation::ENUM_VALUE,
- 'description' => 'Location adjacent to an enum value definition.',
- ],
- 'INPUT_OBJECT' => [
- 'value' => DirectiveLocation::INPUT_OBJECT,
- 'description' => 'Location adjacent to an input object type definition.',
- ],
- 'INPUT_FIELD_DEFINITION' => [
- 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION,
- 'description' => 'Location adjacent to an input object field definition.',
- ],
- ],
- ]);
- }
-
- public static function schemaMetaFieldDef(): FieldDefinition
- {
- return self::$cachedInstances[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([
- 'name' => self::SCHEMA_FIELD_NAME,
- 'type' => Type::nonNull(self::_schema()),
- 'description' => 'Access the current type schema of this server.',
- 'args' => [],
- 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): Schema => $info->schema,
- ]);
- }
-
- public static function typeMetaFieldDef(): FieldDefinition
- {
- return self::$cachedInstances[self::TYPE_FIELD_NAME] ??= new FieldDefinition([
- 'name' => self::TYPE_FIELD_NAME,
- 'type' => self::_type(),
- 'description' => 'Request the type information of a single type.',
- 'args' => [
- [
- 'name' => 'name',
- 'type' => Type::nonNull(Type::string()),
- ],
- ],
- 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): ?Type => $info->schema->getType($args['name']),
- ]);
- }
-
- public static function typeNameMetaFieldDef(): FieldDefinition
- {
- return self::$cachedInstances[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([
- 'name' => self::TYPE_NAME_FIELD_NAME,
- 'type' => Type::nonNull(Type::string()),
- 'description' => 'The name of the current Object type at runtime.',
- 'args' => [],
- 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): string => $info->parentType->name,
- ]);
- }
-
- public static function resetCachedInstances(): void
- {
- self::$cachedInstances = null;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php
deleted file mode 100644
index 30f6f62deb5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php
+++ /dev/null
@@ -1,625 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\InterfaceImplementations;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-/**
- * Schema Definition (see [schema definition docs](schema-definition.md)).
- *
- * A Schema is created by supplying the root types of each type of operation:
- * query, mutation (optional) and subscription (optional). A schema definition is
- * then supplied to the validator and executor. Usage Example:
- *
- * $schema = new Automattic\WooCommerce\Vendor\GraphQL\Type\Schema([
- * 'query' => $MyAppQueryRootType,
- * 'mutation' => $MyAppMutationRootType,
- * ]);
- *
- * Or using Schema Config instance:
- *
- * $config = Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::create()
- * ->setQuery($MyAppQueryRootType)
- * ->setMutation($MyAppMutationRootType);
- *
- * $schema = new Automattic\WooCommerce\Vendor\GraphQL\Type\Schema($config);
- *
- * @phpstan-import-type SchemaConfigOptions from SchemaConfig
- * @phpstan-import-type OperationType from OperationDefinitionNode
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\SchemaTest
- */
-class Schema
-{
- private SchemaConfig $config;
-
- /**
- * Contains currently resolved schema types.
- *
- * @var array<string, Type&NamedType>
- */
- private array $resolvedTypes = [];
-
- /**
- * Lazily initialised.
- *
- * @var array<string, InterfaceImplementations>
- */
- private array $implementationsMap;
-
- /** True when $resolvedTypes contains all possible schema types. */
- private bool $fullyLoaded = false;
-
- /** @var array<string, ScalarType>|null Lazily initialised by getScalarOverrides(). */
- private ?array $scalarOverrides = null;
-
- /** @var array<int, Error> */
- private array $validationErrors;
-
- public ?string $description;
-
- public ?SchemaDefinitionNode $astNode;
-
- /** @var array<SchemaExtensionNode> */
- public array $extensionASTNodes = [];
-
- /**
- * @param SchemaConfig|array<string, mixed> $config
- *
- * @phpstan-param SchemaConfig|SchemaConfigOptions $config
- *
- * @throws InvariantViolation
- *
- * @api
- */
- public function __construct($config)
- {
- if (is_array($config)) {
- $config = SchemaConfig::create($config);
- }
-
- // If this schema was built from a source known to be valid, then it may be
- // marked with assumeValid to avoid an additional type system validation.
- if ($config->getAssumeValid()) {
- $this->validationErrors = [];
- }
-
- $this->description = $config->description;
- $this->astNode = $config->astNode;
- $this->extensionASTNodes = $config->extensionASTNodes;
-
- $this->config = $config;
- }
-
- /**
- * Returns all types in this schema.
- *
- * This operation requires a full schema scan. Do not use in production environment.
- *
- * @throws InvariantViolation
- *
- * @return array<string, Type&NamedType> Keys represent type names, values are instances of corresponding type definitions
- *
- * @api
- */
- public function getTypeMap(): array
- {
- if (! $this->fullyLoaded) {
- // Reset order of user provided types, since calls to getType() may have loaded them
- $this->resolvedTypes = [];
-
- $scalarOverrides = $this->getScalarOverrides();
-
- foreach ($this->materializeTypes() as $typeOrLazyType) {
- /** @var Type|callable(): Type $typeOrLazyType */
- $type = self::resolveType($typeOrLazyType);
- assert($type instanceof NamedType);
-
- /** @var string $typeName Necessary assertion for PHPStan + PHP 8.2 */
- $typeName = $type->name;
-
- if (isset($scalarOverrides[$typeName])) {
- continue;
- }
-
- assert(
- ! isset($this->resolvedTypes[$typeName]) || $type === $this->resolvedTypes[$typeName],
- "Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).",
- );
-
- $this->resolvedTypes[$typeName] = $type;
- }
-
- // To preserve order of user-provided types, we add first to add them to
- // the set of "collected" types, so `collectReferencedTypes` ignore them.
- /** @var array<string, Type&NamedType> $allReferencedTypes */
- $allReferencedTypes = [];
- foreach ($this->resolvedTypes as $type) {
- // When we ready to process this type, we remove it from "collected" types
- // and then add it together with all dependent types in the correct position.
- unset($allReferencedTypes[$type->name]);
- TypeInfo::extractTypes($type, $allReferencedTypes);
- }
-
- foreach ([$this->getQueryType(), $this->getMutationType(), $this->getSubscriptionType()] as $rootType) {
- if ($rootType instanceof ObjectType) {
- TypeInfo::extractTypes($rootType, $allReferencedTypes);
- }
- }
-
- foreach ($this->getDirectives() as $directive) {
- // @phpstan-ignore-next-line generics are not strictly enforceable, error will be caught during schema validation
- if ($directive instanceof Directive) {
- TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes);
- }
- }
- TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes);
-
- // Apply scalar overrides after all extractions, replacing the
- // global singletons with user-provided instances.
- foreach ($scalarOverrides as $name => $override) {
- $allReferencedTypes[$name] = $override;
- }
-
- $this->resolvedTypes = $allReferencedTypes;
- $this->fullyLoaded = true;
- }
-
- return $this->resolvedTypes;
- }
-
- /**
- * Returns a list of directives supported by this schema.
- *
- * @throws InvariantViolation
- *
- * @return array<Directive>
- *
- * @api
- */
- public function getDirectives(): array
- {
- return $this->config->directives ?? GraphQL::getStandardDirectives();
- }
-
- /** @param mixed $typeLoaderReturn could be anything */
- public static function typeLoaderNotType($typeLoaderReturn): string
- {
- $typeClass = Type::class;
- $notType = Utils::printSafe($typeLoaderReturn);
-
- return "Type loader is expected to return an instanceof {$typeClass}, but it returned {$notType}";
- }
-
- public static function typeLoaderWrongTypeName(string $expectedTypeName, string $actualTypeName): string
- {
- return "Type loader is expected to return type {$expectedTypeName}, but it returned type {$actualTypeName}.";
- }
-
- /** Returns root type by operation name. */
- public function getOperationType(string $operation): ?ObjectType
- {
- switch ($operation) {
- case 'query': return $this->getQueryType();
- case 'mutation': return $this->getMutationType();
- case 'subscription': return $this->getSubscriptionType();
- default: return null;
- }
- }
-
- /**
- * Returns root query type.
- *
- * @api
- */
- public function getQueryType(): ?ObjectType
- {
- $query = $this->config->query;
-
- if ($query === null) {
- return null;
- }
-
- if (is_callable($query)) {
- return $this->config->query = $query();
- }
-
- return $query;
- }
-
- /**
- * Returns root mutation type.
- *
- * @api
- */
- public function getMutationType(): ?ObjectType
- {
- $mutation = $this->config->mutation;
-
- if ($mutation === null) {
- return null;
- }
-
- if (is_callable($mutation)) {
- return $this->config->mutation = $mutation();
- }
-
- return $mutation;
- }
-
- /**
- * Returns schema subscription.
- *
- * @api
- */
- public function getSubscriptionType(): ?ObjectType
- {
- $subscription = $this->config->subscription;
-
- if ($subscription === null) {
- return null;
- }
-
- if (is_callable($subscription)) {
- return $this->config->subscription = $subscription();
- }
-
- return $subscription;
- }
-
- /** @api */
- public function getConfig(): SchemaConfig
- {
- return $this->config;
- }
-
- /**
- * Returns a type by name.
- *
- * @throws InvariantViolation
- *
- * @return (Type&NamedType)|null
- *
- * @api
- */
- public function getType(string $name): ?Type
- {
- if (isset($this->resolvedTypes[$name])) {
- return $this->resolvedTypes[$name];
- }
-
- $introspectionTypes = Introspection::getTypes();
- if (isset($introspectionTypes[$name])) {
- return $introspectionTypes[$name];
- }
-
- $type = $this->loadType($name);
- if ($type !== null) {
- return $this->resolvedTypes[$name] = self::resolveType($type);
- }
-
- $scalarOverrides = $this->getScalarOverrides();
- if (isset($scalarOverrides[$name])) {
- return $this->resolvedTypes[$name] = $scalarOverrides[$name];
- }
-
- $builtInScalars = Type::builtInScalars();
- if (isset($builtInScalars[$name])) {
- return $this->resolvedTypes[$name] = $builtInScalars[$name];
- }
-
- return null;
- }
-
- /** @throws InvariantViolation */
- public function hasType(string $name): bool
- {
- return $this->getType($name) !== null;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return (Type&NamedType)|null
- */
- private function loadType(string $typeName): ?Type
- {
- $typeLoader = $this->config->typeLoader;
-
- if (! isset($typeLoader)) {
- return $this->getTypeMap()[$typeName] ?? null;
- }
-
- // TODO https://github.com/webonyx/graphql-php/issues/1874 - reconsider supporting typeLoader-based scalar overrides in the next major version
- if (Type::isBuiltInScalarName($typeName)) {
- return null;
- }
-
- $type = $typeLoader($typeName);
- if ($type === null) {
- return null;
- }
-
- // @phpstan-ignore-next-line not strictly enforceable unless PHP gets function types
- if (! $type instanceof Type) {
- throw new InvariantViolation(self::typeLoaderNotType($type));
- }
-
- if ($typeName !== $type->name) {
- throw new InvariantViolation(self::typeLoaderWrongTypeName($typeName, $type->name));
- }
-
- return $type;
- }
-
- /** @return array<string, ScalarType> */
- private function getScalarOverrides(): array
- {
- if ($this->scalarOverrides === null) {
- $this->scalarOverrides = [];
-
- $builtInScalars = Type::builtInScalars();
- foreach ($this->materializeTypes() as $typeOrLazyType) {
- /** @var Type|callable(): Type $typeOrLazyType */
- $type = self::resolveType($typeOrLazyType);
- if ($type instanceof ScalarType
- && isset($builtInScalars[$type->name])
- && $type !== $builtInScalars[$type->name]
- ) {
- $this->scalarOverrides[$type->name] = $type;
- }
- }
- }
-
- return $this->scalarOverrides;
- }
-
- /**
- * Resolve config->types to an array, materializing callables and generators.
- *
- * @return array<Type|callable(): Type>
- */
- private function materializeTypes(): array
- {
- $types = $this->config->types;
- if (is_callable($types)) {
- $types = $types();
- }
-
- if (! is_array($types)) {
- $types = iterator_to_array($types);
- $this->config->types = $types;
- }
-
- return $types;
- }
-
- /**
- * @template T of Type
- *
- * @param Type|callable $type
- *
- * @phpstan-param T|callable():T $type
- *
- * @phpstan-return T
- */
- public static function resolveType($type): Type
- {
- if ($type instanceof Type) {
- return $type;
- }
-
- return $type();
- }
-
- /**
- * Returns all possible concrete types for given abstract type
- * (implementations for interfaces and members of union type for unions).
- *
- * This operation requires full schema scan. Do not use in production environment.
- *
- * @param AbstractType&Type $abstractType
- *
- * @throws InvariantViolation
- *
- * @return array<ObjectType>
- *
- * @api
- */
- public function getPossibleTypes(AbstractType $abstractType): array
- {
- if ($abstractType instanceof UnionType) {
- return $abstractType->getTypes();
- }
-
- assert($abstractType instanceof InterfaceType, 'only other option');
-
- return $this->getImplementations($abstractType)->objects();
- }
-
- /**
- * Returns all types that implement a given interface type.
- *
- * This operation requires full schema scan. Do not use in production environment.
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public function getImplementations(InterfaceType $abstractType): InterfaceImplementations
- {
- return $this->collectImplementations()[$abstractType->name];
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, InterfaceImplementations>
- */
- private function collectImplementations(): array
- {
- if (! isset($this->implementationsMap)) {
- $this->implementationsMap = [];
-
- /**
- * @var array<
- * string,
- * array{
- * objects: array<int, ObjectType>,
- * interfaces: array<int, InterfaceType>,
- * }
- * > $foundImplementations
- */
- $foundImplementations = [];
- foreach ($this->getTypeMap() as $type) {
- if ($type instanceof InterfaceType) {
- if (! isset($foundImplementations[$type->name])) {
- $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
- }
-
- foreach ($type->getInterfaces() as $iface) {
- if (! isset($foundImplementations[$iface->name])) {
- $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
- }
-
- $foundImplementations[$iface->name]['interfaces'][] = $type;
- }
- } elseif ($type instanceof ObjectType) {
- foreach ($type->getInterfaces() as $iface) {
- if (! isset($foundImplementations[$iface->name])) {
- $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
- }
-
- $foundImplementations[$iface->name]['objects'][] = $type;
- }
- }
- }
-
- foreach ($foundImplementations as $name => $implementations) {
- $this->implementationsMap[$name] = new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
- }
- }
-
- return $this->implementationsMap;
- }
-
- /**
- * Returns true if the given type is a sub type of the given abstract type.
- *
- * @param AbstractType&Type $abstractType
- * @param ImplementingType&Type $maybeSubType
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType): bool
- {
- if ($abstractType instanceof InterfaceType) {
- return $maybeSubType->implementsInterface($abstractType);
- }
-
- assert($abstractType instanceof UnionType, 'only other option');
-
- return $abstractType->isPossibleType($maybeSubType);
- }
-
- /**
- * Returns instance of directive by name.
- *
- * @api
- *
- * @throws InvariantViolation
- */
- public function getDirective(string $name): ?Directive
- {
- foreach ($this->getDirectives() as $directive) {
- if ($directive->name === $name) {
- return $directive;
- }
- }
-
- return null;
- }
-
- /**
- * Throws if the schema is not valid.
- *
- * This operation requires a full schema scan. Do not use in production environment.
- *
- * @throws Error
- * @throws InvariantViolation
- *
- * @api
- */
- public function assertValid(): void
- {
- $errors = $this->validate();
-
- if ($errors !== []) {
- throw new InvariantViolation(implode("\n\n", $this->validationErrors));
- }
-
- $internalTypes = Type::builtInScalars() + Introspection::getTypes();
- foreach ($this->getTypeMap() as $name => $type) {
- if (isset($internalTypes[$name])) {
- continue;
- }
-
- $type->assertValid();
-
- // Make sure type loader returns the same instance as registered in other places of schema
- if (isset($this->config->typeLoader) && $this->loadType($name) !== $type) {
- throw new InvariantViolation("Type loader returns different instance for {$name} than field/argument definitions. Make sure you always return the same instance for the same type name.");
- }
- }
- }
-
- /**
- * Validate the schema and return any errors.
- *
- * This operation requires a full schema scan. Do not use in production environment.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Error>
- *
- * @api
- */
- public function validate(): array
- {
- // If this Schema has already been validated, return the previous results.
- if (isset($this->validationErrors)) {
- return $this->validationErrors;
- }
-
- // Validate the schema, producing a list of errors.
- $context = new SchemaValidationContext($this);
- $context->validateRootTypes();
- $context->validateDirectives();
- $context->validateTypes();
-
- // Persist the results of validation before returning to ensure validation
- // does not run multiple times for this schema.
- $this->validationErrors = $context->getErrors();
-
- return $this->validationErrors;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php
deleted file mode 100644
index b8c12e25960..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php
+++ /dev/null
@@ -1,356 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-
-/**
- * Configuration options for schema construction.
- *
- * The options accepted by the **create** method are described
- * in the [schema definition docs](schema-definition.md#configuration-options).
- *
- * Usage example:
- *
- * $config = SchemaConfig::create()
- * ->setQuery($myQueryType)
- * ->setTypeLoader($myTypeLoader);
- *
- * $schema = new Schema($config);
- *
- * @see Type, NamedType
- *
- * @phpstan-type MaybeLazyObjectType ObjectType|(callable(): (ObjectType|null))|null
- * @phpstan-type TypeLoader callable(string $typeName): ((Type&NamedType)|null)
- * @phpstan-type Types iterable<Type&NamedType>|(callable(): iterable<Type&NamedType>)|iterable<(callable(): Type&NamedType)>|(callable(): iterable<(callable(): Type&NamedType)>)
- * @phpstan-type SchemaConfigOptions array{
- * description?: string|null,
- * query?: MaybeLazyObjectType,
- * mutation?: MaybeLazyObjectType,
- * subscription?: MaybeLazyObjectType,
- * types?: Types|null,
- * directives?: array<Directive>|null,
- * typeLoader?: TypeLoader|null,
- * assumeValid?: bool|null,
- * astNode?: SchemaDefinitionNode|null,
- * extensionASTNodes?: array<SchemaExtensionNode>|null,
- * }
- */
-class SchemaConfig
-{
- public ?string $description = null;
-
- /** @var MaybeLazyObjectType */
- public $query;
-
- /** @var MaybeLazyObjectType */
- public $mutation;
-
- /** @var MaybeLazyObjectType */
- public $subscription;
-
- /**
- * @var iterable|callable
- *
- * @phpstan-var Types
- */
- public $types = [];
-
- /** @var array<Directive>|null */
- public ?array $directives = null;
-
- /**
- * @var callable|null
- *
- * @phpstan-var TypeLoader|null
- */
- public $typeLoader;
-
- public bool $assumeValid = false;
-
- public ?SchemaDefinitionNode $astNode = null;
-
- /** @var array<SchemaExtensionNode> */
- public array $extensionASTNodes = [];
-
- /**
- * Converts an array of options to instance of SchemaConfig
- * (or just returns empty config when array is not passed).
- *
- * @phpstan-param SchemaConfigOptions $options
- *
- * @throws InvariantViolation
- *
- * @api
- */
- public static function create(array $options = []): self
- {
- $config = new static();
-
- if ($options !== []) {
- if (isset($options['description'])) {
- $config->setDescription($options['description']);
- }
- if (isset($options['query'])) {
- $config->setQuery($options['query']);
- }
-
- if (isset($options['mutation'])) {
- $config->setMutation($options['mutation']);
- }
-
- if (isset($options['subscription'])) {
- $config->setSubscription($options['subscription']);
- }
-
- if (isset($options['types'])) {
- $config->setTypes($options['types']);
- }
-
- if (isset($options['directives'])) {
- $config->setDirectives($options['directives']);
- }
-
- if (isset($options['typeLoader'])) {
- $config->setTypeLoader($options['typeLoader']);
- }
-
- if (isset($options['assumeValid'])) {
- $config->setAssumeValid($options['assumeValid']);
- }
-
- if (isset($options['astNode'])) {
- $config->setAstNode($options['astNode']);
- }
-
- if (isset($options['extensionASTNodes'])) {
- $config->setExtensionASTNodes($options['extensionASTNodes']);
- }
- }
-
- return $config;
- }
-
- /** @api */
- public function getDescription(): ?string
- {
- return $this->description;
- }
-
- /** @api */
- public function setDescription(?string $description): self
- {
- $this->description = $description;
-
- return $this;
- }
-
- /**
- * @return MaybeLazyObjectType
- *
- * @api
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * @param MaybeLazyObjectType $query
- *
- * @throws InvariantViolation
- *
- * @api
- */
- public function setQuery($query): self
- {
- $this->assertMaybeLazyObjectType($query);
- $this->query = $query;
-
- return $this;
- }
-
- /**
- * @return MaybeLazyObjectType
- *
- * @api
- */
- public function getMutation()
- {
- return $this->mutation;
- }
-
- /**
- * @param MaybeLazyObjectType $mutation
- *
- * @throws InvariantViolation
- *
- * @api
- */
- public function setMutation($mutation): self
- {
- $this->assertMaybeLazyObjectType($mutation);
- $this->mutation = $mutation;
-
- return $this;
- }
-
- /**
- * @return MaybeLazyObjectType
- *
- * @api
- */
- public function getSubscription()
- {
- return $this->subscription;
- }
-
- /**
- * @param MaybeLazyObjectType $subscription
- *
- * @throws InvariantViolation
- *
- * @api
- */
- public function setSubscription($subscription): self
- {
- $this->assertMaybeLazyObjectType($subscription);
- $this->subscription = $subscription;
-
- return $this;
- }
-
- /**
- * @return array|callable
- *
- * @phpstan-return Types
- *
- * @api
- */
- public function getTypes()
- {
- return $this->types;
- }
-
- /**
- * @param array|callable $types
- *
- * @phpstan-param Types $types
- *
- * @api
- */
- public function setTypes($types): self
- {
- $this->types = $types;
-
- return $this;
- }
-
- /**
- * @return array<Directive>|null
- *
- * @api
- */
- public function getDirectives(): ?array
- {
- return $this->directives;
- }
-
- /**
- * @param array<Directive>|null $directives
- *
- * @api
- */
- public function setDirectives(?array $directives): self
- {
- $this->directives = $directives;
-
- return $this;
- }
-
- /**
- * @return callable|null $typeLoader
- *
- * @phpstan-return TypeLoader|null $typeLoader
- *
- * @api
- */
- public function getTypeLoader(): ?callable
- {
- return $this->typeLoader;
- }
-
- /**
- * @phpstan-param TypeLoader|null $typeLoader
- *
- * @api
- */
- public function setTypeLoader(?callable $typeLoader): self
- {
- $this->typeLoader = $typeLoader;
-
- return $this;
- }
-
- public function getAssumeValid(): bool
- {
- return $this->assumeValid;
- }
-
- public function setAssumeValid(bool $assumeValid): self
- {
- $this->assumeValid = $assumeValid;
-
- return $this;
- }
-
- public function getAstNode(): ?SchemaDefinitionNode
- {
- return $this->astNode;
- }
-
- public function setAstNode(?SchemaDefinitionNode $astNode): self
- {
- $this->astNode = $astNode;
-
- return $this;
- }
-
- /** @return array<SchemaExtensionNode> */
- public function getExtensionASTNodes(): array
- {
- return $this->extensionASTNodes;
- }
-
- /** @param array<SchemaExtensionNode> $extensionASTNodes */
- public function setExtensionASTNodes(array $extensionASTNodes): self
- {
- $this->extensionASTNodes = $extensionASTNodes;
-
- return $this;
- }
-
- /**
- * @param mixed $maybeLazyObjectType Should be MaybeLazyObjectType
- *
- * @throws InvariantViolation
- */
- protected function assertMaybeLazyObjectType($maybeLazyObjectType): void
- {
- if ($maybeLazyObjectType instanceof ObjectType || is_callable($maybeLazyObjectType) || is_null($maybeLazyObjectType)) {
- return;
- }
-
- $notMaybeLazyObjectType = is_object($maybeLazyObjectType)
- ? get_class($maybeLazyObjectType)
- : gettype($maybeLazyObjectType);
- $objectTypeClass = ObjectType::class;
- throw new InvariantViolation("Expected instanceof {$objectTypeClass}, a callable that returns such an instance, or null, got: {$notMaybeLazyObjectType}.");
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php
deleted file mode 100644
index c1625b781d4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php
+++ /dev/null
@@ -1,856 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Validation\InputObjectCircularRefs;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeComparators;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-
-class SchemaValidationContext
-{
- /** @var list<Error> */
- private array $errors = [];
-
- private Schema $schema;
-
- private InputObjectCircularRefs $inputObjectCircularRefs;
-
- public function __construct(Schema $schema)
- {
- $this->schema = $schema;
- $this->inputObjectCircularRefs = new InputObjectCircularRefs($this);
- }
-
- /** @return list<Error> */
- public function getErrors(): array
- {
- return $this->errors;
- }
-
- public function validateRootTypes(): void
- {
- if ($this->schema->getQueryType() === null) {
- $this->reportError('Query root type must be provided.', $this->schema->astNode);
- }
-
- // Triggers a type error if wrong
- $this->schema->getMutationType();
- $this->schema->getSubscriptionType();
- }
-
- /** @param array<Node|null>|Node|null $nodes */
- public function reportError(string $message, $nodes = null): void
- {
- $nodes = array_filter(is_array($nodes) ? $nodes : [$nodes]);
- $this->addError(new Error($message, $nodes));
- }
-
- private function addError(Error $error): void
- {
- $this->errors[] = $error;
- }
-
- /** @throws InvariantViolation */
- public function validateDirectives(): void
- {
- $this->validateDirectiveDefinitions();
-
- // Validate directives that are used on the schema
- $this->validateDirectivesAtLocation(
- $this->getDirectives($this->schema),
- DirectiveLocation::SCHEMA
- );
- }
-
- /** @throws InvariantViolation */
- public function validateDirectiveDefinitions(): void
- {
- $directiveDefinitions = [];
-
- $directives = $this->schema->getDirectives();
- foreach ($directives as $directive) {
- // Ensure all directives are in fact Automattic\WooCommerce\Vendor\GraphQL directives.
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- if (! $directive instanceof Directive) {
- $notDirective = Utils::printSafe($directive);
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- $nodes = is_object($directive) && property_exists($directive, 'astNode')
- ? $directive->astNode
- : null;
-
- $this->reportError(
- "Expected directive but got: {$notDirective}.",
- $nodes
- );
- continue;
- }
-
- $existingDefinitions = $directiveDefinitions[$directive->name] ?? [];
- $existingDefinitions[] = $directive;
- $directiveDefinitions[$directive->name] = $existingDefinitions;
-
- // Ensure they are named correctly.
- $this->validateName($directive);
-
- // TODO: Ensure proper locations.
-
- $argNames = [];
- foreach ($directive->args as $arg) {
- // Ensure they are named correctly.
- $this->validateName($arg);
-
- $argName = $arg->name;
-
- if (isset($argNames[$argName])) {
- $this->reportError(
- "Argument @{$directive->name}({$argName}:) can only be defined once.",
- $this->getAllDirectiveArgNodes($directive, $argName)
- );
- continue;
- }
-
- $argNames[$argName] = true;
-
- // Ensure the type is an input type.
- // @phpstan-ignore-next-line necessary until PHP supports union types
- if (! Type::isInputType($arg->getType())) {
- $type = Utils::printSafe($arg->getType());
- $this->reportError(
- "The type of @{$directive->name}({$argName}:) must be Input Type but got: {$type}.",
- $this->getDirectiveArgTypeNode($directive, $argName)
- );
- }
- }
- }
-
- foreach ($directiveDefinitions as $directiveName => $directiveList) {
- if (count($directiveList) > 1) {
- $nodes = [];
- foreach ($directiveList as $dir) {
- if (isset($dir->astNode)) {
- $nodes[] = $dir->astNode;
- }
- }
-
- $this->reportError(
- "Directive @{$directiveName} defined multiple times.",
- $nodes
- );
- }
- }
- }
-
- /** @param (Type&NamedType)|Directive|FieldDefinition|EnumValueDefinition|InputObjectField|Argument $object */
- private function validateName(object $object): void
- {
- // Ensure names are valid, however introspection types opt out.
- $error = Utils::isValidNameError($object->name, $object->astNode);
- if (
- $error === null
- || ($object instanceof Type && Introspection::isIntrospectionType($object))
- ) {
- return;
- }
-
- $this->addError($error);
- }
-
- /** @return array<int, InputValueDefinitionNode> */
- private function getAllDirectiveArgNodes(Directive $directive, string $argName): array
- {
- $astNode = $directive->astNode;
- if ($astNode === null) {
- return [];
- }
-
- $matchingSubnodes = [];
- foreach ($astNode->arguments as $subNode) {
- if ($subNode->name->value === $argName) {
- $matchingSubnodes[] = $subNode;
- }
- }
-
- return $matchingSubnodes;
- }
-
- /** @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */
- private function getDirectiveArgTypeNode(Directive $directive, string $argName): ?TypeNode
- {
- $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0] ?? null;
-
- return $argNode === null
- ? null
- : $argNode->type;
- }
-
- /** @throws InvariantViolation */
- public function validateTypes(): void
- {
- $typeMap = $this->schema->getTypeMap();
- foreach ($typeMap as $type) {
- // Ensure all provided types are in fact Automattic\WooCommerce\Vendor\GraphQL type.
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- if (! $type instanceof NamedType) {
- $notNamedType = Utils::printSafe($type);
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- $node = $type instanceof Type
- ? $type->astNode
- : null;
-
- $this->reportError("Expected Automattic\WooCommerce\Vendor\GraphQL named type but got: {$notNamedType}.", $node);
- continue;
- }
-
- $this->validateName($type);
-
- if ($type instanceof ObjectType) {
- $this->validateFields($type);
- $this->validateInterfaces($type);
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::OBJECT);
- } elseif ($type instanceof InterfaceType) {
- $this->validateFields($type);
- $this->validateInterfaces($type);
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::IFACE);
- } elseif ($type instanceof UnionType) {
- $this->validateUnionMembers($type);
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::UNION);
- } elseif ($type instanceof EnumType) {
- $this->validateEnumValues($type);
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::ENUM);
- } elseif ($type instanceof InputObjectType) {
- $this->validateInputFields($type);
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::INPUT_OBJECT);
- $this->inputObjectCircularRefs->validate($type);
- } else {
- assert($type instanceof ScalarType, 'only remaining option');
- $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::SCALAR);
- }
- }
- }
-
- /**
- * @param NodeList<DirectiveNode> $directives
- *
- * @throws InvariantViolation
- */
- private function validateDirectivesAtLocation(NodeList $directives, string $location): void
- {
- /** @var array<string, array<int, DirectiveNode>> $potentiallyDuplicateDirectives */
- $potentiallyDuplicateDirectives = [];
- $schema = $this->schema;
- foreach ($directives as $directiveNode) {
- $directiveName = $directiveNode->name->value;
-
- // Ensure directive used is also defined
- $schemaDirective = $schema->getDirective($directiveName);
- if ($schemaDirective === null) {
- $this->reportError("No directive @{$directiveName} defined.", $directiveNode);
- continue;
- }
-
- if (! in_array($location, $schemaDirective->locations, true)) {
- $this->reportError(
- "Directive @{$directiveName} not allowed at {$location} location.",
- array_filter([$directiveNode, $schemaDirective->astNode])
- );
- }
-
- if (! $schemaDirective->isRepeatable) {
- $potentiallyDuplicateDirectives[$directiveName][] = $directiveNode;
- }
- }
-
- foreach ($potentiallyDuplicateDirectives as $directiveName => $directiveList) {
- if (count($directiveList) > 1) {
- $this->reportError("Non-repeatable directive @{$directiveName} used more than once at the same location.", $directiveList);
- }
- }
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @throws InvariantViolation
- */
- private function validateFields(Type $type): void
- {
- $fieldMap = $type->getFields();
-
- if ($fieldMap === []) {
- $this->reportError(
- "Type {$type->name} must define one or more fields.",
- $this->getAllNodes($type)
- );
- }
-
- foreach ($fieldMap as $fieldName => $field) {
- $this->validateName($field);
-
- $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
- if (count($fieldNodes) > 1) {
- $this->reportError("Field {$type->name}.{$fieldName} can only be defined once.", $fieldNodes);
- continue;
- }
-
- $fieldType = $field->getType();
- // @phpstan-ignore-next-line not statically provable until we can use union types
- if (! Type::isOutputType($fieldType)) {
- $safeFieldType = Utils::printSafe($fieldType);
- $this->reportError(
- "The type of {$type->name}.{$fieldName} must be Output Type but got: {$safeFieldType}.",
- $this->getFieldTypeNode($type, $fieldName)
- );
- }
-
- $this->validateTypeIsSingleton($fieldType, "{$type->name}.{$fieldName}");
-
- $argNames = [];
- foreach ($field->args as $arg) {
- $argName = $arg->name;
- $argPath = "{$type->name}.{$fieldName}({$argName}:)";
-
- $this->validateName($arg);
-
- if (isset($argNames[$argName])) {
- $this->reportError(
- "Field argument {$argPath} can only be defined once.",
- $this->getAllFieldArgNodes($type, $fieldName, $argName)
- );
- }
-
- $argNames[$argName] = true;
-
- $argType = $arg->getType();
-
- // @phpstan-ignore-next-line the type of $arg->getType() says it is an input type, but it might not always be true
- if (! Type::isInputType($argType)) {
- $safeType = Utils::printSafe($argType);
- $this->reportError(
- "The type of {$argPath} must be Input Type but got: {$safeType}.",
- $this->getFieldArgTypeNode($type, $fieldName, $argName)
- );
- }
-
- $this->validateTypeIsSingleton($argType, $argPath);
-
- if (isset($arg->astNode->directives)) {
- $this->validateDirectivesAtLocation($arg->astNode->directives, DirectiveLocation::ARGUMENT_DEFINITION);
- }
- }
-
- if (isset($field->astNode->directives)) {
- $this->validateDirectivesAtLocation($field->astNode->directives, DirectiveLocation::FIELD_DEFINITION);
- }
- }
- }
-
- /**
- * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj
- *
- * @return list<SchemaDefinitionNode|SchemaExtensionNode>|list<ObjectTypeDefinitionNode|ObjectTypeExtensionNode>|list<InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode>|list<UnionTypeDefinitionNode|UnionTypeExtensionNode>|list< EnumTypeDefinitionNode|EnumTypeExtensionNode>|list<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode>|list<DirectiveDefinitionNode>
- */
- private function getAllNodes(object $obj): array
- {
- $astNode = $obj->astNode;
-
- if ($obj instanceof Schema) {
- $extensionNodes = $obj->extensionASTNodes;
- } elseif ($obj instanceof Directive) {
- $extensionNodes = [];
- } else {
- $extensionNodes = $obj->extensionASTNodes;
- }
-
- $allNodes = $astNode === null
- ? []
- : [$astNode];
- foreach ($extensionNodes as $extensionNode) {
- $allNodes[] = $extensionNode;
- }
-
- return $allNodes;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @return list<FieldDefinitionNode>
- */
- private function getAllFieldNodes(Type $type, string $fieldName): array
- {
- $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]);
-
- $matchingFieldNodes = [];
-
- foreach ($allNodes as $node) {
- foreach ($node->fields as $field) {
- if ($field->name->value === $fieldName) {
- $matchingFieldNodes[] = $field;
- }
- }
- }
-
- return $matchingFieldNodes;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
- */
- private function getFieldTypeNode(Type $type, string $fieldName): ?TypeNode
- {
- $fieldNode = $this->getFieldNode($type, $fieldName);
-
- return $fieldNode === null
- ? null
- : $fieldNode->type;
- }
-
- /** @param ObjectType|InterfaceType $type */
- private function getFieldNode(Type $type, string $fieldName): ?FieldDefinitionNode
- {
- $nodes = $this->getAllFieldNodes($type, $fieldName);
-
- return $nodes[0] ?? null;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @return array<int, InputValueDefinitionNode>
- */
- private function getAllFieldArgNodes(Type $type, string $fieldName, string $argName): array
- {
- $argNodes = [];
- $fieldNode = $this->getFieldNode($type, $fieldName);
- if ($fieldNode !== null) {
- foreach ($fieldNode->arguments as $node) {
- if ($node->name->value === $argName) {
- $argNodes[] = $node;
- }
- }
- }
-
- return $argNodes;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
- */
- private function getFieldArgTypeNode(Type $type, string $fieldName, string $argName): ?TypeNode
- {
- $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
-
- return $fieldArgNode === null
- ? null
- : $fieldArgNode->type;
- }
-
- /** @param ObjectType|InterfaceType $type */
- private function getFieldArgNode(Type $type, string $fieldName, string $argName): ?InputValueDefinitionNode
- {
- $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
-
- return $nodes[0] ?? null;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @throws InvariantViolation
- */
- private function validateInterfaces(ImplementingType $type): void
- {
- $ifaceTypeNames = [];
- foreach ($type->getInterfaces() as $interface) {
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- if (! $interface instanceof InterfaceType) {
- $notInterface = Utils::printSafe($interface);
- $this->reportError(
- "Type {$type->name} must only implement Interface types, it cannot implement {$notInterface}.",
- $this->getImplementsInterfaceNode($type, $interface)
- );
- continue;
- }
-
- if ($type === $interface) {
- $this->reportError(
- "Type {$type->name} cannot implement itself because it would create a circular reference.",
- $this->getImplementsInterfaceNode($type, $interface)
- );
- continue;
- }
-
- if (isset($ifaceTypeNames[$interface->name])) {
- $this->reportError(
- "Type {$type->name} can only implement {$interface->name} once.",
- $this->getAllImplementsInterfaceNodes($type, $interface)
- );
- continue;
- }
-
- $ifaceTypeNames[$interface->name] = true;
-
- $this->validateTypeImplementsAncestors($type, $interface);
- $this->validateTypeImplementsInterface($type, $interface);
- }
- }
-
- /**
- * @param Schema|(Type&NamedType) $object
- *
- * @return NodeList<DirectiveNode>
- */
- private function getDirectives(object $object): NodeList
- {
- $directives = [];
- /**
- * Excluding directiveNode, since $object is not Directive.
- *
- * @var SchemaDefinitionNode|SchemaExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|EnumTypeDefinitionNode|EnumTypeExtensionNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode $node
- */
- // @phpstan-ignore-next-line union types are not pervasive
- foreach ($this->getAllNodes($object) as $node) {
- foreach ($node->directives as $directive) {
- $directives[] = $directive;
- }
- }
-
- return new NodeList($directives);
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- * @param Type&NamedType $shouldBeInterface
- */
- private function getImplementsInterfaceNode(ImplementingType $type, NamedType $shouldBeInterface): ?NamedTypeNode
- {
- $nodes = $this->getAllImplementsInterfaceNodes($type, $shouldBeInterface);
-
- return $nodes[0] ?? null;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- * @param Type&NamedType $shouldBeInterface
- *
- * @return list<NamedTypeNode>
- */
- private function getAllImplementsInterfaceNodes(ImplementingType $type, NamedType $shouldBeInterface): array
- {
- $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]);
-
- $shouldBeInterfaceName = $shouldBeInterface->name;
- $matchingInterfaceNodes = [];
-
- foreach ($allNodes as $node) {
- foreach ($node->interfaces as $interface) {
- if ($interface->name->value === $shouldBeInterfaceName) {
- $matchingInterfaceNodes[] = $interface;
- }
- }
- }
-
- return $matchingInterfaceNodes;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @throws InvariantViolation
- */
- private function validateTypeImplementsInterface(ImplementingType $type, InterfaceType $iface): void
- {
- $typeFieldMap = $type->getFields();
- $ifaceFieldMap = $iface->getFields();
-
- foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
- $typeField = $typeFieldMap[$fieldName] ?? null;
-
- if ($typeField === null) {
- $this->reportError(
- "Interface field {$iface->name}.{$fieldName} expected but {$type->name} does not provide it.",
- array_merge(
- [$this->getFieldNode($iface, $fieldName)],
- $this->getAllNodes($type)
- )
- );
- continue;
- }
-
- $typeFieldType = $typeField->getType();
- $ifaceFieldType = $ifaceField->getType();
- if (! TypeComparators::isTypeSubTypeOf($this->schema, $typeFieldType, $ifaceFieldType)) {
- $this->reportError(
- "Interface field {$iface->name}.{$fieldName} expects type {$ifaceFieldType} but {$type->name}.{$fieldName} is type {$typeFieldType}.",
- [
- $this->getFieldTypeNode($iface, $fieldName),
- $this->getFieldTypeNode($type, $fieldName),
- ]
- );
- }
-
- foreach ($ifaceField->args as $ifaceArg) {
- $argName = $ifaceArg->name;
- $typeArg = $typeField->getArg($argName);
-
- if ($typeArg === null) {
- $this->reportError(
- "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expected but {$type->name}.{$fieldName} does not provide it.",
- [
- $this->getFieldArgNode($iface, $fieldName, $argName),
- $this->getFieldNode($type, $fieldName),
- ]
- );
- continue;
- }
-
- $ifaceArgType = $ifaceArg->getType();
- $typeArgType = $typeArg->getType();
- if (! TypeComparators::isEqualType($ifaceArgType, $typeArgType)) {
- $this->reportError(
- "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expects type {$ifaceArgType} but {$type->name}.{$fieldName}({$argName}:) is type {$typeArgType}.",
- [
- $this->getFieldArgTypeNode($iface, $fieldName, $argName),
- $this->getFieldArgTypeNode($type, $fieldName, $argName),
- ]
- );
- }
-
- // TODO: validate default values?
- }
-
- foreach ($typeField->args as $typeArg) {
- $argName = $typeArg->name;
- $ifaceArg = $ifaceField->getArg($argName);
-
- if ($typeArg->isRequired() && $ifaceArg === null) {
- $this->reportError(
- "Object field {$type->name}.{$fieldName} includes required argument {$argName} that is missing from the Interface field {$iface->name}.{$fieldName}.",
- [
- $this->getFieldArgNode($type, $fieldName, $argName),
- $this->getFieldNode($iface, $fieldName),
- ]
- );
- }
- }
- }
- }
-
- /** @param ObjectType|InterfaceType $type */
- private function validateTypeImplementsAncestors(ImplementingType $type, InterfaceType $iface): void
- {
- $typeInterfaces = $type->getInterfaces();
- foreach ($iface->getInterfaces() as $transitive) {
- if (! in_array($transitive, $typeInterfaces, true)) {
- $this->reportError(
- $transitive === $type
- ? "Type {$type->name} cannot implement {$iface->name} because it would create a circular reference."
- : "Type {$type->name} must implement {$transitive->name} because it is implemented by {$iface->name}.",
- array_merge(
- $this->getAllImplementsInterfaceNodes($iface, $transitive),
- $this->getAllImplementsInterfaceNodes($type, $iface)
- )
- );
- }
- }
- }
-
- /** @throws InvariantViolation */
- private function validateUnionMembers(UnionType $union): void
- {
- $memberTypes = $union->getTypes();
-
- if ($memberTypes === []) {
- $this->reportError(
- "Union type {$union->name} must define one or more member types.",
- $this->getAllNodes($union)
- );
- }
-
- $includedTypeNames = [];
-
- foreach ($memberTypes as $memberType) {
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- if (! $memberType instanceof ObjectType) {
- $notObjectType = Utils::printSafe($memberType);
- $this->reportError(
- "Union type {$union->name} can only include Object types, it cannot include {$notObjectType}.",
- $this->getUnionMemberTypeNodes($union, $notObjectType)
- );
- continue;
- }
-
- if (isset($includedTypeNames[$memberType->name])) {
- $this->reportError(
- "Union type {$union->name} can only include type {$memberType->name} once.",
- $this->getUnionMemberTypeNodes($union, $memberType->name)
- );
- continue;
- }
-
- $includedTypeNames[$memberType->name] = true;
- }
- }
-
- /** @return list<NamedTypeNode> */
- private function getUnionMemberTypeNodes(UnionType $union, string $typeName): array
- {
- $allNodes = array_filter([$union->astNode, ...$union->extensionASTNodes]);
-
- $types = [];
- foreach ($allNodes as $node) {
- foreach ($node->types as $type) {
- if ($type->name->value === $typeName) {
- $types[] = $type;
- }
- }
- }
-
- return $types;
- }
-
- /** @throws InvariantViolation */
- private function validateEnumValues(EnumType $enumType): void
- {
- $enumValues = $enumType->getValues();
-
- if ($enumValues === []) {
- $this->reportError(
- "Enum type {$enumType->name} must define one or more values.",
- $this->getAllNodes($enumType)
- );
- }
-
- foreach ($enumValues as $enumValue) {
- $valueName = $enumValue->name;
-
- // Ensure valid name.
- $this->validateName($enumValue);
- if (in_array($valueName, ['true', 'false', 'null'], true)) {
- $this->reportError(
- "Enum type {$enumType->name} cannot include value: {$valueName}.",
- $enumValue->astNode
- );
- }
-
- // Ensure valid directives
- if (isset($enumValue->astNode, $enumValue->astNode->directives)) {
- $this->validateDirectivesAtLocation(
- $enumValue->astNode->directives,
- DirectiveLocation::ENUM_VALUE
- );
- }
- }
- }
-
- /** @throws InvariantViolation */
- private function validateInputFields(InputObjectType $inputObj): void
- {
- $fieldMap = $inputObj->getFields();
-
- if ($fieldMap === []) {
- $this->reportError(
- "Input Object type {$inputObj->name} must define one or more fields.",
- $this->getAllNodes($inputObj)
- );
- }
-
- // Ensure the arguments are valid
- foreach ($fieldMap as $fieldName => $field) {
- // Ensure they are named correctly.
- $this->validateName($field);
-
- // TODO: Ensure they are unique per field.
-
- // Ensure the type is an input type.
- $type = $field->getType();
- // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
- if (! Type::isInputType($type)) {
- $notInputType = Utils::printSafe($type);
- $this->reportError(
- "The type of {$inputObj->name}.{$fieldName} must be Input Type but got: {$notInputType}.",
- $field->astNode->type ?? null
- );
- }
-
- // Ensure valid directives
- if (isset($field->astNode, $field->astNode->directives)) {
- $this->validateDirectivesAtLocation(
- $field->astNode->directives,
- DirectiveLocation::INPUT_FIELD_DEFINITION
- );
- }
- }
- }
-
- /** @throws InvariantViolation */
- private function validateTypeIsSingleton(Type $type, string $path): void
- {
- $schemaConfig = $this->schema->getConfig();
- if (! isset($schemaConfig->typeLoader)) {
- return;
- }
-
- $namedType = Type::getNamedType($type);
- if ($namedType->isBuiltInType()) {
- return;
- }
-
- $name = $namedType->name;
- if ($namedType !== ($schemaConfig->typeLoader)($name)) {
- throw new InvariantViolation(static::duplicateType($this->schema, $path, $name));
- }
- }
-
- public static function duplicateType(Schema $schema, string $path, string $name): string
- {
- $hint = isset($schema->getConfig()->typeLoader)
- ? 'Ensure the type loader returns the same instance. '
- : '';
-
- return "Found duplicate type in schema at {$path}: {$name}. {$hint}See https://webonyx.github.io/graphql-php/type-definitions/#type-registry.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php b/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php
deleted file mode 100644
index cbe234c7edd..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
-
-class TypeKind
-{
- public const SCALAR = 'SCALAR';
- public const OBJECT = 'OBJECT';
- public const INTERFACE = 'INTERFACE';
- public const UNION = 'UNION';
- public const ENUM = 'ENUM';
- public const INPUT_OBJECT = 'INPUT_OBJECT';
- public const LIST = 'LIST';
- public const NON_NULL = 'NON_NULL';
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php
deleted file mode 100644
index 84992b19799..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Validation;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaValidationContext;
-
-class InputObjectCircularRefs
-{
- private SchemaValidationContext $schemaValidationContext;
-
- /**
- * Tracks already visited types to maintain O(N) and to ensure that cycles
- * are not redundantly reported.
- *
- * @var array<string, bool>
- */
- private array $visitedTypes = [];
-
- /** @var array<int, InputObjectField> */
- private array $fieldPath = [];
-
- /**
- * Position in the type path.
- *
- * @var array<string, int>
- */
- private array $fieldPathIndexByTypeName = [];
-
- public function __construct(SchemaValidationContext $schemaValidationContext)
- {
- $this->schemaValidationContext = $schemaValidationContext;
- }
-
- /**
- * This does a straight-forward DFS to find cycles.
- * It does not terminate when a cycle was found but continues to explore
- * the graph to find all possible cycles.
- *
- * @throws InvariantViolation
- */
- public function validate(InputObjectType $inputObj): void
- {
- if (isset($this->visitedTypes[$inputObj->name])) {
- return;
- }
-
- $this->visitedTypes[$inputObj->name] = true;
- $this->fieldPathIndexByTypeName[$inputObj->name] = count($this->fieldPath);
-
- $fieldMap = $inputObj->getFields();
- foreach ($fieldMap as $field) {
- $type = $field->getType();
-
- if ($type instanceof NonNull) {
- $fieldType = $type->getWrappedType();
-
- // If the type of the field is anything else then a non-nullable input object,
- // there is no chance of an unbreakable cycle
- if ($fieldType instanceof InputObjectType) {
- $this->fieldPath[] = $field;
-
- if (! isset($this->fieldPathIndexByTypeName[$fieldType->name])) {
- $this->validate($fieldType);
- } else {
- $cycleIndex = $this->fieldPathIndexByTypeName[$fieldType->name];
- $cyclePath = array_slice($this->fieldPath, $cycleIndex);
- $fieldNames = implode(
- '.',
- array_map(
- static fn (InputObjectField $field): string => $field->name,
- $cyclePath
- )
- );
- $fieldNodes = array_map(
- static fn (InputObjectField $field): ?InputValueDefinitionNode => $field->astNode,
- $cyclePath
- );
-
- $this->schemaValidationContext->reportError(
- "Cannot reference Input Object \"{$fieldType->name}\" within itself through a series of non-null fields: \"{$fieldNames}\".",
- $fieldNodes
- );
- }
- }
- }
-
- array_pop($this->fieldPath);
- }
-
- unset($this->fieldPathIndexByTypeName[$inputObj->name]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php
deleted file mode 100644
index defad21dc1d..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php
+++ /dev/null
@@ -1,631 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Location;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IDType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NullableType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * Various utilities dealing with AST.
- */
-class AST
-{
- /**
- * Convert representation of AST as an associative array to instance of Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node.
- *
- * For example:
- *
- * ```php
- * AST::fromArray([
- * 'kind' => 'ListValue',
- * 'values' => [
- * ['kind' => 'StringValue', 'value' => 'my str'],
- * ['kind' => 'StringValue', 'value' => 'my other str']
- * ],
- * 'loc' => ['start' => 21, 'end' => 25]
- * ]);
- * ```
- *
- * Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList`
- * returning instances of `StringValueNode` on access.
- *
- * This is a reverse operation for AST::toArray($node)
- *
- * @param array<string, mixed> $node
- *
- * @api
- *
- * @throws \JsonException
- * @throws InvariantViolation
- */
- public static function fromArray(array $node): Node
- {
- $kind = $node['kind'] ?? null;
- if ($kind === null) {
- $safeNode = Utils::printSafeJson($node);
- throw new InvariantViolation("Node is missing kind: {$safeNode}");
- }
-
- $class = NodeKind::CLASS_MAP[$kind] ?? null;
- if ($class === null) {
- $safeNode = Utils::printSafeJson($node);
- throw new InvariantViolation("Node has unexpected kind: {$safeNode}");
- }
-
- $instance = new $class([]);
-
- if (isset($node['loc']['start'], $node['loc']['end'])) {
- $instance->loc = Location::create($node['loc']['start'], $node['loc']['end']);
- }
-
- foreach ($node as $key => $value) {
- if ($key === 'loc' || $key === 'kind') {
- continue;
- }
-
- if (is_array($value)) {
- $value = isset($value[0]) || $value === []
- ? new NodeList($value)
- : self::fromArray($value);
- }
-
- $instance->{$key} = $value;
- }
-
- return $instance;
- }
-
- /**
- * Convert AST node to serializable array.
- *
- * @return array<string, mixed>
- *
- * @api
- */
- public static function toArray(Node $node): array
- {
- return $node->toArray();
- }
-
- /**
- * Produces a Automattic\WooCommerce\Vendor\GraphQL Value AST given a PHP value.
- *
- * Optionally, a Automattic\WooCommerce\Vendor\GraphQL type may be provided, which will be used to
- * disambiguate between value primitives.
- *
- * | PHP Value | Automattic\WooCommerce\Vendor\GraphQL Value |
- * | ------------- | -------------------- |
- * | Object | Input Object |
- * | Assoc Array | Input Object |
- * | Array | List |
- * | Boolean | Boolean |
- * | String | String / Enum Value |
- * | Int | Int |
- * | Float | Int / Float |
- * | Mixed | Enum Value |
- * | null | NullValue |
- *
- * @param mixed $value
- * @param InputType&Type $type
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- *
- * @return (ValueNode&Node)|null
- *
- * @api
- */
- public static function astFromValue($value, InputType $type): ?ValueNode
- {
- if ($type instanceof NonNull) {
- $wrappedType = $type->getWrappedType();
- assert($wrappedType instanceof InputType);
-
- $astValue = self::astFromValue($value, $wrappedType);
-
- return $astValue instanceof NullValueNode
- ? null
- : $astValue;
- }
-
- if ($value === null) {
- return new NullValueNode([]);
- }
-
- // Convert PHP iterables to Automattic\WooCommerce\Vendor\GraphQL list. If the GraphQLType is a list, but
- // the value is not an array, convert the value using the list's item type.
- if ($type instanceof ListOfType) {
- $itemType = $type->getWrappedType();
- assert($itemType instanceof InputType, 'proven by schema validation');
-
- if (is_iterable($value)) {
- $valuesNodes = [];
- foreach ($value as $item) {
- $itemNode = self::astFromValue($item, $itemType);
- if ($itemNode !== null) {
- $valuesNodes[] = $itemNode;
- }
- }
-
- return new ListValueNode(['values' => new NodeList($valuesNodes)]);
- }
-
- return self::astFromValue($value, $itemType);
- }
-
- // Populate the fields of the input object by creating ASTs from each value
- // in the PHP object according to the fields in the input type.
- if ($type instanceof InputObjectType) {
- $isArray = is_array($value);
- $isArrayLike = $isArray || $value instanceof \ArrayAccess;
- if (! $isArrayLike && ! is_object($value)) {
- return null;
- }
-
- $fields = $type->getFields();
- $fieldNodes = [];
- foreach ($fields as $fieldName => $field) {
- $fieldValue = $isArrayLike
- ? $value[$fieldName] ?? null
- : $value->{$fieldName} ?? null;
-
- // Have to check additionally if key exists, since we differentiate between
- // "no key" and "value is null":
- if ($fieldValue !== null) {
- $fieldExists = true;
- } elseif ($isArray) {
- $fieldExists = array_key_exists($fieldName, $value);
- } elseif ($isArrayLike) {
- $fieldExists = $value->offsetExists($fieldName);
- } else {
- $fieldExists = property_exists($value, $fieldName);
- }
-
- if (! $fieldExists) {
- continue;
- }
-
- $fieldNode = self::astFromValue($fieldValue, $field->getType());
-
- if ($fieldNode === null) {
- continue;
- }
-
- $fieldNodes[] = new ObjectFieldNode([
- 'name' => new NameNode(['value' => $fieldName]),
- 'value' => $fieldNode,
- ]);
- }
-
- return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]);
- }
-
- assert($type instanceof LeafType, 'other options were exhausted');
-
- // Since value is an internally represented value, it must be serialized
- // to an externally represented value before converting into an AST.
- $serialized = $type->serialize($value);
-
- // Others serialize based on their corresponding PHP scalar types.
- if (is_bool($serialized)) {
- return new BooleanValueNode(['value' => $serialized]);
- }
-
- if (is_int($serialized)) {
- return new IntValueNode(['value' => (string) $serialized]);
- }
-
- if (is_float($serialized)) {
- /** @phpstan-ignore equal.notAllowed (int cast with == used for performance reasons) */
- if ((int) $serialized == $serialized) {
- return new IntValueNode(['value' => (string) $serialized]);
- }
-
- return new FloatValueNode(['value' => (string) $serialized]);
- }
-
- if (is_string($serialized)) {
- // Enum types use Enum literals.
- if ($type instanceof EnumType) {
- return new EnumValueNode(['value' => $serialized]);
- }
-
- // ID types can use Int literals.
- $asInt = (int) $serialized;
- if ($type instanceof IDType && (string) $asInt === $serialized) {
- return new IntValueNode(['value' => $serialized]);
- }
-
- // Use json_encode, which uses the same string encoding as GraphQL,
- // then remove the quotes.
- return new StringValueNode(['value' => $serialized]);
- }
-
- $notConvertible = Utils::printSafe($serialized);
- throw new InvariantViolation("Cannot convert value to AST: {$notConvertible}");
- }
-
- /**
- * Produces a PHP value given a Automattic\WooCommerce\Vendor\GraphQL Value AST.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL type must be provided, which will be used to interpret different
- * Automattic\WooCommerce\Vendor\GraphQL Value literals.
- *
- * Returns `null` when the value could not be validly coerced according to
- * the provided type.
- *
- * | Automattic\WooCommerce\Vendor\GraphQL Value | PHP Value |
- * | -------------------- | ------------- |
- * | Input Object | Assoc Array |
- * | List | Array |
- * | Boolean | Boolean |
- * | String | String |
- * | Int / Float | Int / Float |
- * | Enum Value | Mixed |
- * | Null Value | null |
- *
- * @param (ValueNode&Node)|null $valueNode
- * @param array<string, mixed>|null $variables
- *
- * @throws \Exception
- *
- * @return mixed
- *
- * @api
- */
- public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null, ?Schema $schema = null)
- {
- $undefined = Utils::undefined();
-
- if ($valueNode === null) {
- // When there is no AST, then there is also no value.
- // Importantly, this is different from returning the Automattic\WooCommerce\Vendor\GraphQL null value.
- return $undefined;
- }
-
- if ($type instanceof NonNull) {
- if ($valueNode instanceof NullValueNode) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- return self::valueFromAST($valueNode, $type->getWrappedType(), $variables, $schema);
- }
-
- if ($valueNode instanceof NullValueNode) {
- // This is explicitly returning the value null.
- return null;
- }
-
- if ($valueNode instanceof VariableNode) {
- $variableName = $valueNode->name->value;
-
- if ($variables === null || ! array_key_exists($variableName, $variables)) {
- // No valid return value.
- return $undefined;
- }
-
- // Note: This does no further checking that this variable is correct.
- // This assumes that this query has been validated and the variable
- // usage here is of the correct type.
- return $variables[$variableName];
- }
-
- if ($type instanceof ListOfType) {
- $itemType = $type->getWrappedType();
-
- if ($valueNode instanceof ListValueNode) {
- $coercedValues = [];
- $itemNodes = $valueNode->values;
- foreach ($itemNodes as $itemNode) {
- if (self::isMissingVariable($itemNode, $variables)) {
- // If an array contains a missing variable, it is either coerced to
- // null or if the item type is non-null, it considered invalid.
- if ($itemType instanceof NonNull) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- $coercedValues[] = null;
- } else {
- $itemValue = self::valueFromAST($itemNode, $itemType, $variables, $schema);
- if ($undefined === $itemValue) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- $coercedValues[] = $itemValue;
- }
- }
-
- return $coercedValues;
- }
-
- $coercedValue = self::valueFromAST($valueNode, $itemType, $variables, $schema);
- if ($undefined === $coercedValue) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- return [$coercedValue];
- }
-
- if ($type instanceof InputObjectType) {
- if (! $valueNode instanceof ObjectValueNode) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- $coercedObj = [];
- $fields = $type->getFields();
-
- $fieldNodes = [];
- foreach ($valueNode->fields as $field) {
- $fieldNodes[$field->name->value] = $field;
- }
-
- foreach ($fields as $field) {
- $fieldName = $field->name;
- $fieldNode = $fieldNodes[$fieldName] ?? null;
-
- if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) {
- if ($field->defaultValueExists()) {
- $coercedObj[$fieldName] = $field->defaultValue;
- } elseif ($field->getType() instanceof NonNull) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- continue;
- }
-
- $fieldValue = self::valueFromAST(
- $fieldNode->value,
- $field->getType(),
- $variables,
- $schema,
- );
-
- if ($undefined === $fieldValue) {
- // Invalid: intentionally return no value.
- return $undefined;
- }
-
- $coercedObj[$fieldName] = $fieldValue;
- }
-
- return $type->parseValue($coercedObj);
- }
-
- if ($type instanceof EnumType) {
- try {
- return $type->parseLiteral($valueNode, $variables);
- } catch (\Throwable $error) {
- return $undefined;
- }
- }
-
- assert($type instanceof ScalarType, 'only remaining option');
- $typeName = $type->name;
-
- // Account for type loader returning a different scalar instance than
- // the built-in singleton used in field definitions. Resolve the actual
- // type from the schema to ensure the correct parseLiteral() is called.
- if ($schema !== null && Type::isBuiltInScalarName($typeName)) {
- $schemaType = $schema->getType($typeName);
- assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$typeName}\".");
- $type = $schemaType;
- }
-
- // Scalars fulfill parsing a literal value via parseLiteral().
- // Invalid values represent a failure to parse correctly, in which case
- // no value is returned.
- try {
- return $type->parseLiteral($valueNode, $variables);
- } catch (\Throwable $error) {
- return $undefined;
- }
- }
-
- /**
- * Returns true if the provided valueNode is a variable which is not defined
- * in the set of variables.
- *
- * @param ValueNode&Node $valueNode
- * @param array<string, mixed>|null $variables
- */
- private static function isMissingVariable(ValueNode $valueNode, ?array $variables): bool
- {
- return $valueNode instanceof VariableNode
- && ($variables === null || ! array_key_exists($valueNode->name->value, $variables));
- }
-
- /**
- * Produces a PHP value given a Automattic\WooCommerce\Vendor\GraphQL Value AST.
- *
- * Unlike `valueFromAST()`, no type is provided. The resulting PHP value
- * will reflect the provided Automattic\WooCommerce\Vendor\GraphQL value AST.
- *
- * | Automattic\WooCommerce\Vendor\GraphQL Value | PHP Value |
- * | -------------------- | ------------- |
- * | Input Object | Assoc Array |
- * | List | Array |
- * | Boolean | Boolean |
- * | String | String |
- * | Int / Float | Int / Float |
- * | Enum | Mixed |
- * | Null | null |
- *
- * @param array<string, mixed>|null $variables
- *
- * @throws \Exception
- *
- * @return mixed
- *
- * @api
- */
- public static function valueFromASTUntyped(Node $valueNode, ?array $variables = null)
- {
- switch (true) {
- case $valueNode instanceof NullValueNode:
- return null;
-
- case $valueNode instanceof IntValueNode:
- return (int) $valueNode->value;
-
- case $valueNode instanceof FloatValueNode:
- return (float) $valueNode->value;
-
- case $valueNode instanceof StringValueNode:
- case $valueNode instanceof EnumValueNode:
- case $valueNode instanceof BooleanValueNode:
- return $valueNode->value;
-
- case $valueNode instanceof ListValueNode:
- $values = [];
- foreach ($valueNode->values as $node) {
- $values[] = self::valueFromASTUntyped($node, $variables);
- }
-
- return $values;
-
- case $valueNode instanceof ObjectValueNode:
- $values = [];
- foreach ($valueNode->fields as $field) {
- $values[$field->name->value] = self::valueFromASTUntyped($field->value, $variables);
- }
-
- return $values;
-
- case $valueNode instanceof VariableNode:
- $variableName = $valueNode->name->value;
-
- return ($variables ?? []) !== [] && isset($variables[$variableName])
- ? $variables[$variableName]
- : null;
- }
-
- throw new Error("Unexpected value kind: {$valueNode->kind}");
- }
-
- /**
- * Returns type definition for given AST Type node.
- *
- * @param callable(string): ?Type $typeLoader
- * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
- *
- * @throws \Exception
- *
- * @api
- */
- public static function typeFromAST(callable $typeLoader, Node $inputTypeNode): ?Type
- {
- if ($inputTypeNode instanceof ListTypeNode) {
- $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type);
-
- return $innerType === null
- ? null
- : new ListOfType($innerType);
- }
-
- if ($inputTypeNode instanceof NonNullTypeNode) {
- $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type);
- if ($innerType === null) {
- return null;
- }
-
- assert($innerType instanceof NullableType, 'proven by schema validation');
-
- return new NonNull($innerType);
- }
-
- return $typeLoader($inputTypeNode->name->value);
- }
-
- /**
- * Returns the operation within a document by name.
- *
- * If a name is not provided, an operation is only returned if the document has exactly one.
- *
- * @api
- */
- public static function getOperationAST(DocumentNode $document, ?string $operationName = null): ?OperationDefinitionNode
- {
- $operation = null;
- foreach ($document->definitions->getIterator() as $node) {
- if (! $node instanceof OperationDefinitionNode) {
- continue;
- }
-
- if ($operationName === null) {
- // We found a second operation, so we bail instead of returning an ambiguous result.
- if ($operation !== null) {
- return null;
- }
-
- $operation = $node;
- } elseif ($node->name instanceof NameNode && $node->name->value === $operationName) {
- return $node;
- }
- }
-
- return $operation;
- }
-
- /**
- * Provided a collection of ASTs, presumably each from different files,
- * concatenate the ASTs together into batched AST, useful for validating many
- * Automattic\WooCommerce\Vendor\GraphQL source files which together represent one conceptual application.
- *
- * @param array<DocumentNode> $documents
- *
- * @api
- */
- public static function concatAST(array $documents): DocumentNode
- {
- /** @var array<int, Node&DefinitionNode> $definitions */
- $definitions = [];
- foreach ($documents as $document) {
- foreach ($document->definitions as $definition) {
- $definitions[] = $definition;
- }
- }
-
- return new DocumentNode(['definitions' => new NodeList($definitions)]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php
deleted file mode 100644
index b96d6fe5f12..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php
+++ /dev/null
@@ -1,651 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-
-/**
- * @see FieldDefinition, InputObjectField
- *
- * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
- * @phpstan-import-type InputObjectFieldConfig from InputObjectField
- * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
- *
- * @phpstan-type ResolveType callable(string, Node|null): (Type&NamedType)
- * @phpstan-type TypeConfigDecorator callable(array<string, mixed>, Node&TypeDefinitionNode, array<string, Node&TypeDefinitionNode>): array<string, mixed>
- * @phpstan-type FieldConfigDecorator callable(UnnamedFieldDefinitionConfig, FieldDefinitionNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode): UnnamedFieldDefinitionConfig
- */
-class ASTDefinitionBuilder
-{
- /** @var array<string, Node&TypeDefinitionNode> */
- private array $typeDefinitionsMap;
-
- /**
- * @var callable
- *
- * @phpstan-var ResolveType
- */
- private $resolveType;
-
- /**
- * @var callable|null
- *
- * @phpstan-var TypeConfigDecorator|null
- */
- private $typeConfigDecorator;
-
- /**
- * @var callable|null
- *
- * @phpstan-var FieldConfigDecorator|null
- */
- private $fieldConfigDecorator;
-
- /** @var array<string, Type&NamedType> */
- private array $cache;
-
- /** @var array<string, array<int, Node&TypeExtensionNode>> */
- private array $typeExtensionsMap;
-
- /**
- * @param array<string, Node&TypeDefinitionNode> $typeDefinitionsMap
- * @param array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap
- *
- * @phpstan-param ResolveType $resolveType
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- *
- * @throws InvariantViolation
- */
- public function __construct(
- array $typeDefinitionsMap,
- array $typeExtensionsMap,
- callable $resolveType,
- ?callable $typeConfigDecorator = null,
- ?callable $fieldConfigDecorator = null
- ) {
- $this->typeDefinitionsMap = $typeDefinitionsMap;
- $this->typeExtensionsMap = $typeExtensionsMap;
- $this->resolveType = $resolveType;
- $this->typeConfigDecorator = $typeConfigDecorator;
- $this->fieldConfigDecorator = $fieldConfigDecorator;
-
- $this->cache = Type::builtInTypes();
- }
-
- /** @throws \Exception */
- public function buildDirective(DirectiveDefinitionNode $directiveNode): Directive
- {
- $locations = [];
- foreach ($directiveNode->locations as $location) {
- $locations[] = $location->value;
- }
-
- return new Directive([
- 'name' => $directiveNode->name->value,
- 'description' => $directiveNode->description->value ?? null,
- 'args' => $this->makeInputValues($directiveNode->arguments),
- 'isRepeatable' => $directiveNode->repeatable,
- 'locations' => $locations,
- 'astNode' => $directiveNode,
- ]);
- }
-
- /**
- * @param NodeList<InputValueDefinitionNode> $values
- *
- * @throws \Exception
- *
- * @return array<string, UnnamedInputObjectFieldConfig>
- */
- private function makeInputValues(NodeList $values): array
- {
- /** @var array<string, UnnamedInputObjectFieldConfig> $map */
- $map = [];
- foreach ($values as $value) {
- // Note: While this could make assertions to get the correctly typed
- // value, that would throw immediately while type system validation
- // with validateSchema() will produce more actionable results.
- /** @var Type&InputType $type */
- $type = $this->buildWrappedType($value->type);
-
- $config = [
- 'name' => $value->name->value,
- 'type' => $type,
- 'description' => $value->description->value ?? null,
- 'deprecationReason' => $this->getDeprecationReason($value),
- 'astNode' => $value,
- ];
-
- if ($value->defaultValue !== null) {
- $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
- }
-
- $map[$value->name->value] = $config;
- }
-
- return $map;
- }
-
- /**
- * @param array<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode> $nodes
- *
- * @throws \Exception
- *
- * @return array<string, UnnamedInputObjectFieldConfig>
- */
- private function makeInputFields(array $nodes): array
- {
- /** @var array<int, InputValueDefinitionNode> $fields */
- $fields = [];
- foreach ($nodes as $node) {
- array_push($fields, ...$node->fields);
- }
-
- return $this->makeInputValues(new NodeList($fields));
- }
-
- /**
- * @param ListTypeNode|NonNullTypeNode|NamedTypeNode $typeNode
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- */
- private function buildWrappedType(TypeNode $typeNode): Type
- {
- if ($typeNode instanceof ListTypeNode) {
- return Type::listOf($this->buildWrappedType($typeNode->type));
- }
-
- if ($typeNode instanceof NonNullTypeNode) {
- // @phpstan-ignore-next-line contained type is NullableType
- return Type::nonNull($this->buildWrappedType($typeNode->type));
- }
-
- return $this->buildType($typeNode);
- }
-
- /**
- * @param string|(Node&NamedTypeNode)|(Node&TypeDefinitionNode) $ref
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @return Type&NamedType
- */
- public function buildType($ref): Type
- {
- if ($ref instanceof TypeDefinitionNode) {
- return $this->internalBuildType($ref->getName()->value, $ref);
- }
- if ($ref instanceof NamedTypeNode) {
- return $this->internalBuildType($ref->name->value, $ref);
- }
-
- return $this->internalBuildType($ref);
- }
-
- /**
- * Calling this method is an equivalent of `typeMap[typeName]` in `graphql-js`.
- * It is legal to access a type from the map of already-built types that doesn't exist in the map.
- * Since we build types lazily, and we don't have a such map of built types,
- * this method provides a way to build a type that may not exist in the SDL definitions and returns null instead.
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @return (Type&NamedType)|null
- */
- public function maybeBuildType(string $name): ?Type
- {
- return isset($this->typeDefinitionsMap[$name])
- ? $this->buildType($name)
- : null;
- }
-
- /**
- * @param (Node&NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @return Type&NamedType
- */
- private function internalBuildType(string $typeName, ?Node $typeNode = null): Type
- {
- if (isset($this->cache[$typeName])) {
- return $this->cache[$typeName];
- }
-
- if (isset($this->typeDefinitionsMap[$typeName])) {
- $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
-
- if ($this->typeConfigDecorator !== null) {
- try {
- $config = ($this->typeConfigDecorator)(
- $type->config,
- $this->typeDefinitionsMap[$typeName],
- $this->typeDefinitionsMap
- );
- } catch (\Throwable $e) {
- $class = static::class;
- throw new Error("Type config decorator passed to {$class} threw an error when building {$typeName} type: {$e->getMessage()}", null, null, [], null, $e);
- }
-
- // @phpstan-ignore-next-line should not happen, but function types are not enforced by PHP
- if (! is_array($config) || isset($config[0])) {
- $class = static::class;
- $notArray = Utils::printSafe($config);
- throw new Error("Type config decorator passed to {$class} is expected to return an array, but got {$notArray}");
- }
-
- $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
- }
-
- return $this->cache[$typeName] = $type;
- }
-
- return $this->cache[$typeName] = ($this->resolveType)($typeName, $typeNode);
- }
-
- /**
- * @param TypeDefinitionNode&Node $def
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- *
- * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
- */
- private function makeSchemaDef(Node $def): Type
- {
- switch (true) {
- case $def instanceof ObjectTypeDefinitionNode:
- return $this->makeTypeDef($def);
-
- case $def instanceof InterfaceTypeDefinitionNode:
- return $this->makeInterfaceDef($def);
-
- case $def instanceof EnumTypeDefinitionNode:
- return $this->makeEnumDef($def);
-
- case $def instanceof UnionTypeDefinitionNode:
- return $this->makeUnionDef($def);
-
- case $def instanceof ScalarTypeDefinitionNode:
- return $this->makeScalarDef($def);
-
- default:
- assert($def instanceof InputObjectTypeDefinitionNode, 'all implementations are known');
-
- return $this->makeInputObjectDef($def);
- }
- }
-
- /** @throws InvariantViolation */
- private function makeTypeDef(ObjectTypeDefinitionNode $def): ObjectType
- {
- $name = $def->name->value;
- /** @var array<ObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
- $allNodes = [$def, ...$extensionASTNodes];
-
- return new ObjectType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- 'fields' => fn (): array => $this->makeFieldDefMap($allNodes),
- 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes),
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /**
- * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes
- *
- * @throws \Exception
- *
- * @phpstan-return array<string, UnnamedFieldDefinitionConfig>
- */
- private function makeFieldDefMap(array $nodes): array
- {
- $map = [];
- foreach ($nodes as $node) {
- foreach ($node->fields as $field) {
- $map[$field->name->value] = $this->buildField($field, $node);
- }
- }
-
- return $map;
- }
-
- /**
- * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $node
- *
- * @throws \Exception
- * @throws Error
- *
- * @return UnnamedFieldDefinitionConfig
- */
- public function buildField(FieldDefinitionNode $field, object $node): array
- {
- // Note: While this could make assertions to get the correctly typed
- // value, that would throw immediately while type system validation
- // with validateSchema() will produce more actionable results.
- /** @var OutputType&Type $type */
- $type = $this->buildWrappedType($field->type);
-
- $config = [
- 'type' => $type,
- 'description' => $field->description->value ?? null,
- 'args' => $this->makeInputValues($field->arguments),
- 'deprecationReason' => $this->getDeprecationReason($field),
- 'astNode' => $field,
- ];
-
- if ($this->fieldConfigDecorator !== null) {
- $config = ($this->fieldConfigDecorator)($config, $field, $node);
- }
-
- return $config;
- }
-
- /**
- * Given a collection of directives, returns the string value for the
- * deprecation reason.
- *
- * @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- private function getDeprecationReason(Node $node): ?string
- {
- $deprecated = Values::getDirectiveValues(
- Directive::deprecatedDirective(),
- $node
- );
-
- return $deprecated['reason'] ?? null;
- }
-
- /**
- * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<int, InterfaceType>
- */
- private function makeImplementedInterfaces(array $nodes): array
- {
- // Note: While this could make early assertions to get the correctly
- // typed values, that would throw immediately while type system
- // validation with validateSchema() will produce more actionable results.
-
- $interfaces = [];
- foreach ($nodes as $node) {
- foreach ($node->interfaces as $interface) {
- $interfaces[] = $this->buildType($interface);
- }
- }
-
- // @phpstan-ignore-next-line generic type will be validated during schema validation
- return $interfaces;
- }
-
- /** @throws InvariantViolation */
- private function makeInterfaceDef(InterfaceTypeDefinitionNode $def): InterfaceType
- {
- $name = $def->name->value;
- /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
- $allNodes = [$def, ...$extensionASTNodes];
-
- return new InterfaceType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- 'fields' => fn (): array => $this->makeFieldDefMap($allNodes),
- 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes),
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- private function makeEnumDef(EnumTypeDefinitionNode $def): EnumType
- {
- $name = $def->name->value;
- /** @var array<EnumTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
-
- $values = [];
- foreach ([$def, ...$extensionASTNodes] as $node) {
- foreach ($node->values as $value) {
- $values[$value->name->value] = [
- 'description' => $value->description->value ?? null,
- 'deprecationReason' => $this->getDeprecationReason($value),
- 'astNode' => $value,
- ];
- }
- }
-
- return new EnumType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- 'values' => $values,
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /** @throws InvariantViolation */
- private function makeUnionDef(UnionTypeDefinitionNode $def): UnionType
- {
- $name = $def->name->value;
- /** @var array<UnionTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
-
- return new UnionType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- // Note: While this could make assertions to get the correctly typed
- // values below, that would throw immediately while type system
- // validation with validateSchema() will produce more actionable results.
- 'types' => function () use ($def, $extensionASTNodes): array {
- $types = [];
- foreach ([$def, ...$extensionASTNodes] as $node) {
- foreach ($node->types as $type) {
- $types[] = $this->buildType($type);
- }
- }
-
- /** @var array<int, ObjectType> $types */
- return $types;
- },
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /** @throws InvariantViolation */
- private function makeScalarDef(ScalarTypeDefinitionNode $def): CustomScalarType
- {
- $name = $def->name->value;
- /** @var array<ScalarTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
-
- return new CustomScalarType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- 'serialize' => static fn ($value) => $value,
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- private function makeInputObjectDef(InputObjectTypeDefinitionNode $def): InputObjectType
- {
- $name = $def->name->value;
- /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
- $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
-
- $oneOfDirective = Directive::oneOfDirective();
-
- // Check for @oneOf directive in the definition node
- $isOneOf = Values::getDirectiveValues($oneOfDirective, $def) !== null;
-
- // Check for @oneOf directive in extension nodes
- if (! $isOneOf) {
- foreach ($extensionASTNodes as $extensionNode) {
- if (Values::getDirectiveValues($oneOfDirective, $extensionNode) !== null) {
- $isOneOf = true;
- break;
- }
- }
- }
-
- return new InputObjectType([
- 'name' => $name,
- 'description' => $def->description->value ?? null,
- 'isOneOf' => $isOneOf,
- 'fields' => fn (): array => $this->makeInputFields([$def, ...$extensionASTNodes]),
- 'astNode' => $def,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /**
- * @param array<string, mixed> $config
- *
- * @throws Error
- *
- * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
- */
- private function makeSchemaDefFromConfig(Node $def, array $config): Type
- {
- switch (true) {
- case $def instanceof ObjectTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new ObjectType($config);
-
- case $def instanceof InterfaceTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new InterfaceType($config);
-
- case $def instanceof EnumTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new EnumType($config);
-
- case $def instanceof UnionTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new UnionType($config);
-
- case $def instanceof ScalarTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new CustomScalarType($config);
-
- case $def instanceof InputObjectTypeDefinitionNode:
- // @phpstan-ignore-next-line assume the config matches
- return new InputObjectType($config);
-
- default:
- throw new Error("Type kind of {$def->kind} not supported.");
- }
- }
-
- /**
- * @throws \Exception
- *
- * @return InputObjectFieldConfig
- */
- public function buildInputField(InputValueDefinitionNode $value): array
- {
- $type = $this->buildWrappedType($value->type);
- assert($type instanceof InputType, 'proven by schema validation');
-
- $config = [
- 'name' => $value->name->value,
- 'type' => $type,
- 'description' => $value->description->value ?? null,
- 'astNode' => $value,
- ];
-
- if ($value->defaultValue !== null) {
- $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
- }
-
- return $config;
- }
-
- /**
- * @throws \Exception
- *
- * @return array<string, mixed>
- */
- public function buildEnumValue(EnumValueDefinitionNode $value): array
- {
- return [
- 'description' => $value->description->value ?? null,
- 'deprecationReason' => $this->getDeprecationReason($value),
- 'astNode' => $value,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php
deleted file mode 100644
index ebc43cc87f3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php
+++ /dev/null
@@ -1,944 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * Utility for finding breaking/dangerous changes between two schemas.
- *
- * @phpstan-type Change array{type: string, description: string}
- * @phpstan-type Changes array{
- * breakingChanges: array<int, Change>,
- * dangerousChanges: array<int, Change>
- * }
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BreakingChangesFinderTest
- */
-class BreakingChangesFinder
-{
- public const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND';
- public const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
- public const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND';
- public const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
- public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
- public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
- public const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
- public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND';
- public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED';
- public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED';
- public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED';
- public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED';
- public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED';
- public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED';
- public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED';
- public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE';
- public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
- public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED';
- public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
- public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED';
- public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED';
-
- /**
- * Given two schemas, returns an Array containing descriptions of all the types
- * of breaking changes covered by the other functions down below.
- *
- * @throws \TypeError
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema): array
- {
- return array_merge(
- self::findRemovedTypes($oldSchema, $newSchema),
- self::findTypesThatChangedKind($oldSchema, $newSchema),
- self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
- self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
- self::findTypesRemovedFromUnions($oldSchema, $newSchema),
- self::findValuesRemovedFromEnums($oldSchema, $newSchema),
- self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
- self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
- self::findRemovedDirectives($oldSchema, $newSchema),
- self::findRemovedDirectiveArgs($oldSchema, $newSchema),
- self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
- self::findRemovedDirectiveLocations($oldSchema, $newSchema)
- );
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any breaking
- * changes in the newSchema related to removing an entire type.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findRemovedTypes(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $breakingChanges = [];
- foreach (array_keys($oldTypeMap) as $typeName) {
- if (! isset($newTypeMap[$typeName])) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_TYPE_REMOVED,
- 'description' => "{$typeName} was removed.",
- ];
- }
- }
-
- return $breakingChanges;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any breaking
- * changes in the newSchema related to changing the type of a type.
- *
- * @throws \TypeError
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findTypesThatChangedKind(
- Schema $schemaA,
- Schema $schemaB
- ): array {
- $schemaATypeMap = $schemaA->getTypeMap();
- $schemaBTypeMap = $schemaB->getTypeMap();
-
- $breakingChanges = [];
- foreach ($schemaATypeMap as $typeName => $schemaAType) {
- if (! isset($schemaBTypeMap[$typeName])) {
- continue;
- }
-
- $schemaBType = $schemaBTypeMap[$typeName];
- if ($schemaAType instanceof $schemaBType) {
- continue;
- }
-
- if ($schemaBType instanceof $schemaAType) {
- continue;
- }
-
- $schemaATypeKindName = self::typeKindName($schemaAType);
- $schemaBTypeKindName = self::typeKindName($schemaBType);
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
- 'description' => "{$typeName} changed from {$schemaATypeKindName} to {$schemaBTypeKindName}.",
- ];
- }
-
- return $breakingChanges;
- }
-
- /**
- * @param Type&NamedType $type
- *
- * @throws \TypeError
- */
- private static function typeKindName(NamedType $type): string
- {
- if ($type instanceof ScalarType) {
- return 'a Scalar type';
- }
-
- if ($type instanceof ObjectType) {
- return 'an Object type';
- }
-
- if ($type instanceof InterfaceType) {
- return 'an Interface type';
- }
-
- if ($type instanceof UnionType) {
- return 'a Union type';
- }
-
- if ($type instanceof EnumType) {
- return 'an Enum type';
- }
-
- if ($type instanceof InputObjectType) {
- return 'an Input type';
- }
-
- throw new \TypeError('Unknown type: ' . $type->name);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $breakingChanges = [];
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (
- ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
- || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
- || ! ($newType instanceof $oldType)
- ) {
- continue;
- }
-
- $oldTypeFieldsDef = $oldType->getFields();
- $newTypeFieldsDef = $newType->getFields();
- foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
- // Check if the field is missing on the type in the new schema.
- if (! isset($newTypeFieldsDef[$fieldName])) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
- 'description' => "{$typeName}.{$fieldName} was removed.",
- ];
- } else {
- $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
- $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
- $isSafe = self::isChangeSafeForObjectOrInterfaceField(
- $oldFieldType,
- $newFieldType
- );
- if (! $isSafe) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
- 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldType} to {$newFieldType}.",
- ];
- }
- }
- }
- }
-
- return $breakingChanges;
- }
-
- private static function isChangeSafeForObjectOrInterfaceField(
- Type $oldType,
- Type $newType
- ): bool {
- if ($oldType instanceof NamedType) {
- return // if they're both named types, see if their names are equivalent
- ($newType instanceof NamedType && $oldType->name === $newType->name)
- // moving from nullable to non-null of the same underlying type is safe
- || ($newType instanceof NonNull
- && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
- }
-
- if ($oldType instanceof ListOfType) {
- return // if they're both lists, make sure the underlying types are compatible
- ($newType instanceof ListOfType && self::isChangeSafeForObjectOrInterfaceField(
- $oldType->getWrappedType(),
- $newType->getWrappedType()
- ))
- // moving from nullable to non-null of the same underlying type is safe
- || ($newType instanceof NonNull
- && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
- }
-
- if ($oldType instanceof NonNull) {
- // if they're both non-null, make sure the underlying types are compatible
- return $newType instanceof NonNull
- && self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
- }
-
- return false;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return Changes
- */
- public static function findFieldsThatChangedTypeOnInputObjectTypes(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $breakingChanges = [];
- $dangerousChanges = [];
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
- continue;
- }
-
- $oldTypeFieldsDef = $oldType->getFields();
- $newTypeFieldsDef = $newType->getFields();
- foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
- if (! isset($newTypeFieldsDef[$fieldName])) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
- 'description' => "{$typeName}.{$fieldName} was removed.",
- ];
- } else {
- $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
- $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
-
- $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
- $oldFieldType,
- $newFieldType
- );
- if (! $isSafe) {
- $oldFieldTypeString = $oldFieldType instanceof NamedType
- ? $oldFieldType->name
- : $oldFieldType;
- $newFieldTypeString = $newFieldType instanceof NamedType
- ? $newFieldType->name
- : $newFieldType;
-
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
- 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldTypeString} to {$newFieldTypeString}.",
- ];
- }
- }
- }
-
- // Check if a field was added to the input object type
- foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
- if (isset($oldTypeFieldsDef[$fieldName])) {
- continue;
- }
-
- $newTypeName = $newType->name;
- if ($fieldDef->isRequired()) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED,
- 'description' => "A required field {$fieldName} on input type {$newTypeName} was added.",
- ];
- } else {
- $dangerousChanges[] = [
- 'type' => self::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED,
- 'description' => "An optional field {$fieldName} on input type {$newTypeName} was added.",
- ];
- }
- }
- }
-
- return [
- 'breakingChanges' => $breakingChanges,
- 'dangerousChanges' => $dangerousChanges,
- ];
- }
-
- /** @throws InvariantViolation */
- private static function isChangeSafeForInputObjectFieldOrFieldArg(
- Type $oldType,
- Type $newType
- ): bool {
- if ($oldType instanceof NamedType) {
- if (! $newType instanceof NamedType) {
- return false;
- }
-
- // if they're both named types, see if their names are equivalent
- return $oldType->name === $newType->name;
- }
-
- if ($oldType instanceof ListOfType) {
- // if they're both lists, make sure the underlying types are compatible
- return $newType instanceof ListOfType
- && self::isChangeSafeForInputObjectFieldOrFieldArg(
- $oldType->getWrappedType(),
- $newType->getWrappedType()
- );
- }
-
- if ($oldType instanceof NonNull) {
- return // if they're both non-null, make sure the underlying types are compatible
- ($newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg(
- $oldType->getWrappedType(),
- $newType->getWrappedType()
- ))
- // moving from non-null to nullable of the same underlying type is safe
- || ! ($newType instanceof NonNull)
- && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
- }
-
- return false;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any breaking
- * changes in the newSchema related to removing types from a union type.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findTypesRemovedFromUnions(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $typesRemovedFromUnion = [];
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
- continue;
- }
-
- $typeNamesInNewUnion = [];
- foreach ($newType->getTypes() as $type) {
- $typeNamesInNewUnion[$type->name] = true;
- }
-
- foreach ($oldType->getTypes() as $type) {
- if (! isset($typeNamesInNewUnion[$type->name])) {
- $typesRemovedFromUnion[] = [
- 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
- 'description' => "{$type->name} was removed from union type {$typeName}.",
- ];
- }
- }
- }
-
- return $typesRemovedFromUnion;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any breaking
- * changes in the newSchema related to removing values from an enum type.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findValuesRemovedFromEnums(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $valuesRemovedFromEnums = [];
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
- continue;
- }
-
- $valuesInNewEnum = [];
- foreach ($newType->getValues() as $value) {
- $valuesInNewEnum[$value->name] = true;
- }
-
- foreach ($oldType->getValues() as $value) {
- if (! isset($valuesInNewEnum[$value->name])) {
- $valuesRemovedFromEnums[] = [
- 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
- 'description' => "{$value->name} was removed from enum type {$typeName}.",
- ];
- }
- }
- }
-
- return $valuesRemovedFromEnums;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any
- * breaking or dangerous changes in the newSchema related to arguments
- * (such as removal or change of type of an argument, or a change in an
- * argument's default value).
- *
- * @throws InvariantViolation
- *
- * @return Changes
- */
- public static function findArgChanges(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $breakingChanges = [];
- $dangerousChanges = [];
-
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (
- ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
- || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
- || ! ($newType instanceof $oldType)
- ) {
- continue;
- }
-
- $oldTypeFields = $oldType->getFields();
- $newTypeFields = $newType->getFields();
-
- foreach ($oldTypeFields as $fieldName => $oldField) {
- if (! isset($newTypeFields[$fieldName])) {
- continue;
- }
-
- foreach ($oldField->args as $oldArgDef) {
- $newArgDef = null;
- foreach ($newTypeFields[$fieldName]->args as $newArg) {
- if ($newArg->name === $oldArgDef->name) {
- $newArgDef = $newArg;
- }
- }
-
- if ($newArgDef !== null) {
- $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
- $oldArgDef->getType(),
- $newArgDef->getType()
- );
- $oldArgType = $oldArgDef->getType();
- $oldArgName = $oldArgDef->name;
- if (! $isSafe) {
- $newArgType = $newArgDef->getType();
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
- 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed type from {$oldArgType} to {$newArgType}",
- ];
- } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
- $dangerousChanges[] = [
- 'type' => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
- 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed defaultValue",
- ];
- }
- } else {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_ARG_REMOVED,
- 'description' => "{$typeName}.{$fieldName} arg {$oldArgDef->name} was removed",
- ];
- }
-
- // Check if arg was added to the field
- foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
- $oldArgDef = null;
- foreach ($oldTypeFields[$fieldName]->args as $oldArg) {
- if ($oldArg->name === $newTypeFieldArgDef->name) {
- $oldArgDef = $oldArg;
- }
- }
-
- if ($oldArgDef !== null) {
- continue;
- }
-
- $newTypeName = $newType->name;
- $newArgName = $newTypeFieldArgDef->name;
- if ($newTypeFieldArgDef->isRequired()) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_REQUIRED_ARG_ADDED,
- 'description' => "A required arg {$newArgName} on {$newTypeName}.{$fieldName} was added",
- ];
- } else {
- $dangerousChanges[] = [
- 'type' => self::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED,
- 'description' => "An optional arg {$newArgName} on {$newTypeName}.{$fieldName} was added",
- ];
- }
- }
- }
- }
- }
-
- return [
- 'breakingChanges' => $breakingChanges,
- 'dangerousChanges' => $dangerousChanges,
- ];
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findInterfacesRemovedFromObjectTypes(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
- $breakingChanges = [];
-
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) {
- continue;
- }
-
- $oldInterfaces = $oldType->getInterfaces();
- $newInterfaces = $newType->getInterfaces();
- foreach ($oldInterfaces as $oldInterface) {
- $interfaceWasRemoved = true;
- foreach ($newInterfaces as $newInterface) {
- if ($oldInterface->name === $newInterface->name) {
- $interfaceWasRemoved = false;
- }
- }
-
- if ($interfaceWasRemoved) {
- $breakingChanges[] = [
- 'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED,
- 'description' => "{$typeName} no longer implements interface {$oldInterface->name}.",
- ];
- }
- }
- }
-
- return $breakingChanges;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema): array
- {
- $removedDirectives = [];
-
- $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
- foreach ($oldSchema->getDirectives() as $directive) {
- if (! isset($newSchemaDirectiveMap[$directive->name])) {
- $removedDirectives[] = [
- 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
- 'description' => "{$directive->name} was removed",
- ];
- }
- }
-
- return $removedDirectives;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<string, Directive>
- */
- private static function getDirectiveMapForSchema(Schema $schema): array
- {
- $directives = [];
- foreach ($schema->getDirectives() as $directive) {
- $directives[$directive->name] = $directive;
- }
-
- return $directives;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema): array
- {
- $removedDirectiveArgs = [];
- $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
-
- foreach ($newSchema->getDirectives() as $newDirective) {
- if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
- continue;
- }
-
- foreach (
- self::findRemovedArgsForDirectives(
- $oldSchemaDirectiveMap[$newDirective->name],
- $newDirective
- ) as $arg
- ) {
- $removedDirectiveArgs[] = [
- 'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
- 'description' => "{$arg->name} was removed from {$newDirective->name}",
- ];
- }
- }
-
- return $removedDirectiveArgs;
- }
-
- /** @return array<int, Argument> */
- public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective): array
- {
- $removedArgs = [];
- $newArgMap = self::getArgumentMapForDirective($newDirective);
- foreach ($oldDirective->args as $arg) {
- if (! isset($newArgMap[$arg->name])) {
- $removedArgs[] = $arg;
- }
- }
-
- return $removedArgs;
- }
-
- /** @return array<string, Argument> */
- private static function getArgumentMapForDirective(Directive $directive): array
- {
- $args = [];
- foreach ($directive->args as $arg) {
- $args[$arg->name] = $arg;
- }
-
- return $args;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema): array
- {
- $addedNonNullableArgs = [];
- $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
-
- foreach ($newSchema->getDirectives() as $newDirective) {
- if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
- continue;
- }
-
- foreach (
- self::findAddedArgsForDirective(
- $oldSchemaDirectiveMap[$newDirective->name],
- $newDirective
- ) as $arg
- ) {
- if ($arg->isRequired()) {
- $addedNonNullableArgs[] = [
- 'type' => self::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED,
- 'description' => "A required arg {$arg->name} on directive {$newDirective->name} was added",
- ];
- }
- }
- }
-
- return $addedNonNullableArgs;
- }
-
- /** @return array<int, Argument> */
- public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective): array
- {
- $addedArgs = [];
- $oldArgMap = self::getArgumentMapForDirective($oldDirective);
- foreach ($newDirective->args as $arg) {
- if (! isset($oldArgMap[$arg->name])) {
- $addedArgs[] = $arg;
- }
- }
-
- return $addedArgs;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema): array
- {
- $removedLocations = [];
- $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
-
- foreach ($newSchema->getDirectives() as $newDirective) {
- if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
- continue;
- }
-
- foreach (
- self::findRemovedLocationsForDirective(
- $oldSchemaDirectiveMap[$newDirective->name],
- $newDirective
- ) as $location
- ) {
- $removedLocations[] = [
- 'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
- 'description' => "{$location} was removed from {$newDirective->name}",
- ];
- }
- }
-
- return $removedLocations;
- }
-
- /** @return array<int, string> */
- public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective): array
- {
- $removedLocations = [];
- $newLocationSet = array_flip($newDirective->locations);
- foreach ($oldDirective->locations as $oldLocation) {
- if (! array_key_exists($oldLocation, $newLocationSet)) {
- $removedLocations[] = $oldLocation;
- }
- }
-
- return $removedLocations;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of all the types
- * of potentially dangerous changes covered by the other functions down below.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema): array
- {
- return array_merge(
- self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
- self::findValuesAddedToEnums($oldSchema, $newSchema),
- self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
- self::findTypesAddedToUnions($oldSchema, $newSchema),
- self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
- );
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any dangerous
- * changes in the newSchema related to adding values to an enum type.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findValuesAddedToEnums(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $valuesAddedToEnums = [];
- foreach ($oldTypeMap as $typeName => $oldType) {
- $newType = $newTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
- continue;
- }
-
- $valuesInOldEnum = [];
- foreach ($oldType->getValues() as $value) {
- $valuesInOldEnum[$value->name] = true;
- }
-
- foreach ($newType->getValues() as $value) {
- if (! isset($valuesInOldEnum[$value->name])) {
- $valuesAddedToEnums[] = [
- 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
- 'description' => "{$value->name} was added to enum type {$typeName}.",
- ];
- }
- }
- }
-
- return $valuesAddedToEnums;
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findInterfacesAddedToObjectTypes(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
- $interfacesAddedToObjectTypes = [];
-
- foreach ($newTypeMap as $typeName => $newType) {
- $oldType = $oldTypeMap[$typeName] ?? null;
- if (
- ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
- || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
- ) {
- continue;
- }
-
- $oldInterfaces = $oldType->getInterfaces();
- $newInterfaces = $newType->getInterfaces();
- foreach ($newInterfaces as $newInterface) {
- $interfaceWasAdded = true;
- foreach ($oldInterfaces as $oldInterface) {
- if ($oldInterface->name === $newInterface->name) {
- $interfaceWasAdded = false;
- }
- }
-
- if ($interfaceWasAdded) {
- $interfacesAddedToObjectTypes[] = [
- 'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED,
- 'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.",
- ];
- }
- }
- }
-
- return $interfacesAddedToObjectTypes;
- }
-
- /**
- * Given two schemas, returns an Array containing descriptions of any dangerous
- * changes in the newSchema related to adding types to a union type.
- *
- * @throws InvariantViolation
- *
- * @return array<int, Change>
- */
- public static function findTypesAddedToUnions(
- Schema $oldSchema,
- Schema $newSchema
- ): array {
- $oldTypeMap = $oldSchema->getTypeMap();
- $newTypeMap = $newSchema->getTypeMap();
-
- $typesAddedToUnion = [];
- foreach ($newTypeMap as $typeName => $newType) {
- $oldType = $oldTypeMap[$typeName] ?? null;
- if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
- continue;
- }
-
- $typeNamesInOldUnion = [];
- foreach ($oldType->getTypes() as $type) {
- $typeNamesInOldUnion[$type->name] = true;
- }
-
- foreach ($newType->getTypes() as $type) {
- if (! isset($typeNamesInOldUnion[$type->name])) {
- $typesAddedToUnion[] = [
- 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
- 'description' => "{$type->name} was added to union type {$typeName}.",
- ];
- }
- }
- }
-
- return $typesAddedToUnion;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php
deleted file mode 100644
index a892eb1a472..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php
+++ /dev/null
@@ -1,563 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\TypeKind;
-
-/**
- * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
- * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
- *
- * @phpstan-type Options array{
- * assumeValid?: bool
- * }
- *
- * - assumeValid:
- * When building a schema from a Automattic\WooCommerce\Vendor\GraphQL service's introspection result, it
- * might be safe to assume the schema is valid. Set to true to assume the
- * produced schema is valid.
- *
- * Default: false
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BuildClientSchemaTest
- */
-class BuildClientSchema
-{
- /** @var array<string, mixed> */
- private array $introspection;
-
- /**
- * @var array<string, bool>
- *
- * @phpstan-var Options
- */
- private array $options;
-
- /** @var array<string, NamedType&Type> */
- private array $typeMap = [];
-
- /**
- * @param array<string, mixed> $introspectionQuery
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- */
- public function __construct(array $introspectionQuery, array $options = [])
- {
- $this->introspection = $introspectionQuery;
- $this->options = $options;
- }
-
- /**
- * Build a schema for use by client tools.
- *
- * Given the result of a client running the introspection query, creates and
- * returns a \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema instance which can be then used with all graphql-php
- * tools, but cannot be used to execute a query, as introspection does not
- * represent the "resolver", "parse" or "serialize" functions or any other
- * server-internal mechanisms.
- *
- * This function expects a complete introspection result. Don't forget to check
- * the "errors" field of a server response before calling this function.
- *
- * @param array<string, mixed> $introspectionQuery
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @api
- *
- * @throws \Exception
- * @throws InvariantViolation
- */
- public static function build(array $introspectionQuery, array $options = []): Schema
- {
- return (new self($introspectionQuery, $options))->buildSchema();
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function buildSchema(): Schema
- {
- if (! array_key_exists('__schema', $this->introspection)) {
- $missingSchemaIntrospection = Utils::printSafeJson($this->introspection);
- throw new InvariantViolation("Invalid or incomplete introspection result. Ensure that you are passing \"data\" property of introspection response and no \"errors\" was returned alongside: {$missingSchemaIntrospection}.");
- }
-
- $schemaIntrospection = $this->introspection['__schema'];
-
- $builtInTypes = array_merge(
- Type::builtInScalars(),
- Introspection::getTypes()
- );
-
- foreach ($schemaIntrospection['types'] as $typeIntrospection) {
- if (! isset($typeIntrospection['name'])) {
- throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection);
- }
-
- $name = $typeIntrospection['name'];
- if (! is_string($name)) {
- throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection);
- }
-
- // Use the built-in singleton types to avoid reconstruction
- $this->typeMap[$name] = $builtInTypes[$name]
- ?? $this->buildType($typeIntrospection);
- }
-
- $description = isset($schemaIntrospection['description'])
- ? $schemaIntrospection['description']
- : null;
-
- $queryType = isset($schemaIntrospection['queryType'])
- ? $this->getObjectType($schemaIntrospection['queryType'])
- : null;
-
- $mutationType = isset($schemaIntrospection['mutationType'])
- ? $this->getObjectType($schemaIntrospection['mutationType'])
- : null;
-
- $subscriptionType = isset($schemaIntrospection['subscriptionType'])
- ? $this->getObjectType($schemaIntrospection['subscriptionType'])
- : null;
-
- $directives = isset($schemaIntrospection['directives'])
- ? array_map(
- [$this, 'buildDirective'],
- $schemaIntrospection['directives']
- )
- : [];
-
- return new Schema(
- (new SchemaConfig())
- ->setDescription($description)
- ->setQuery($queryType)
- ->setMutation($mutationType)
- ->setSubscription($subscriptionType)
- ->setTypes($this->typeMap)
- ->setDirectives($directives)
- ->setAssumeValid($this->options['assumeValid'] ?? false)
- );
- }
-
- /**
- * @param array<string, mixed> $typeRef
- *
- * @throws InvariantViolation
- */
- private function getType(array $typeRef): Type
- {
- if (isset($typeRef['kind'])) {
- if ($typeRef['kind'] === TypeKind::LIST) {
- if (! isset($typeRef['ofType'])) {
- throw new InvariantViolation('Decorated type deeper than introspection query.');
- }
-
- return new ListOfType($this->getType($typeRef['ofType']));
- }
-
- if ($typeRef['kind'] === TypeKind::NON_NULL) {
- if (! isset($typeRef['ofType'])) {
- throw new InvariantViolation('Decorated type deeper than introspection query.');
- }
-
- // @phpstan-ignore-next-line if the type is not a nullable type, schema validation will catch it
- return new NonNull($this->getType($typeRef['ofType']));
- }
- }
-
- if (! isset($typeRef['name'])) {
- $unknownTypeRef = Utils::printSafeJson($typeRef);
- throw new InvariantViolation("Unknown type reference: {$unknownTypeRef}.");
- }
-
- return $this->getNamedType($typeRef['name']);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @return NamedType&Type
- */
- private function getNamedType(string $typeName): NamedType
- {
- if (! isset($this->typeMap[$typeName])) {
- throw new InvariantViolation("Invalid or incomplete schema, unknown type: {$typeName}. Ensure that a full introspection query is used in order to build a client schema.");
- }
-
- return $this->typeMap[$typeName];
- }
-
- /** @param array<mixed> $type */
- public static function invalidOrIncompleteIntrospectionResult(array $type): InvariantViolation
- {
- $incompleteType = Utils::printSafeJson($type);
-
- return new InvariantViolation("Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: {$incompleteType}.");
- }
-
- /**
- * @param array<string, mixed> $typeRef
- *
- * @throws InvariantViolation
- *
- * @return Type&InputType
- */
- private function getInputType(array $typeRef): InputType
- {
- $type = $this->getType($typeRef);
-
- if ($type instanceof InputType) {
- return $type;
- }
-
- $notInputType = Utils::printSafe($type);
- throw new InvariantViolation("Introspection must provide input type for arguments, but received: {$notInputType}.");
- }
-
- /**
- * @param array<string, mixed> $typeRef
- *
- * @throws InvariantViolation
- */
- private function getOutputType(array $typeRef): OutputType
- {
- $type = $this->getType($typeRef);
-
- if ($type instanceof OutputType) {
- return $type;
- }
-
- $notInputType = Utils::printSafe($type);
- throw new InvariantViolation("Introspection must provide output type for fields, but received: {$notInputType}.");
- }
-
- /**
- * @param array<string, mixed> $typeRef
- *
- * @throws InvariantViolation
- */
- private function getObjectType(array $typeRef): ObjectType
- {
- $type = $this->getType($typeRef);
-
- return ObjectType::assertObjectType($type);
- }
-
- /**
- * @param array<string, mixed> $typeRef
- *
- * @throws InvariantViolation
- */
- public function getInterfaceType(array $typeRef): InterfaceType
- {
- $type = $this->getType($typeRef);
-
- return InterfaceType::assertInterfaceType($type);
- }
-
- /**
- * @param array<string, mixed> $type
- *
- * @throws InvariantViolation
- *
- * @return Type&NamedType
- */
- private function buildType(array $type): NamedType
- {
- if (! array_key_exists('kind', $type)) {
- throw self::invalidOrIncompleteIntrospectionResult($type);
- }
-
- switch ($type['kind']) {
- case TypeKind::SCALAR:
- return $this->buildScalarDef($type);
- case TypeKind::OBJECT:
- return $this->buildObjectDef($type);
- case TypeKind::INTERFACE:
- return $this->buildInterfaceDef($type);
- case TypeKind::UNION:
- return $this->buildUnionDef($type);
- case TypeKind::ENUM:
- return $this->buildEnumDef($type);
- case TypeKind::INPUT_OBJECT:
- return $this->buildInputObjectDef($type);
- default:
- $unknownKindType = Utils::printSafeJson($type);
- throw new InvariantViolation("Invalid or incomplete introspection result. Received type with unknown kind: {$unknownKindType}.");
- }
- }
-
- /**
- * @param array<string, string> $scalar
- *
- * @throws InvariantViolation
- */
- private function buildScalarDef(array $scalar): ScalarType
- {
- return new CustomScalarType([
- 'name' => $scalar['name'],
- 'description' => $scalar['description'],
- 'serialize' => static fn ($value) => $value,
- ]);
- }
-
- /**
- * @param array<string, mixed> $implementingIntrospection
- *
- * @throws InvariantViolation
- *
- * @return array<int, InterfaceType>
- */
- private function buildImplementationsList(array $implementingIntrospection): array
- {
- // TODO: Temporary workaround until Automattic\WooCommerce\Vendor\GraphQL ecosystem will fully support 'interfaces' on interface types.
- if (
- array_key_exists('interfaces', $implementingIntrospection)
- && $implementingIntrospection['interfaces'] === null
- && $implementingIntrospection['kind'] === TypeKind::INTERFACE
- ) {
- return [];
- }
-
- if (! array_key_exists('interfaces', $implementingIntrospection)) {
- $safeIntrospection = Utils::printSafeJson($implementingIntrospection);
- throw new InvariantViolation("Introspection result missing interfaces: {$safeIntrospection}.");
- }
-
- return array_map(
- [$this, 'getInterfaceType'],
- $implementingIntrospection['interfaces']
- );
- }
-
- /**
- * @param array<string, mixed> $object
- *
- * @throws InvariantViolation
- */
- private function buildObjectDef(array $object): ObjectType
- {
- return new ObjectType([
- 'name' => $object['name'],
- 'description' => $object['description'],
- 'interfaces' => fn (): array => $this->buildImplementationsList($object),
- 'fields' => fn (): array => $this->buildFieldDefMap($object),
- ]);
- }
-
- /**
- * @param array<string, mixed> $interface
- *
- * @throws InvariantViolation
- */
- private function buildInterfaceDef(array $interface): InterfaceType
- {
- return new InterfaceType([
- 'name' => $interface['name'],
- 'description' => $interface['description'],
- 'fields' => fn (): array => $this->buildFieldDefMap($interface),
- 'interfaces' => fn (): array => $this->buildImplementationsList($interface),
- ]);
- }
-
- /**
- * @param array<string, mixed> $union
- *
- * @throws InvariantViolation
- */
- private function buildUnionDef(array $union): UnionType
- {
- if (! array_key_exists('possibleTypes', $union)) {
- $safeUnion = Utils::printSafeJson($union);
- throw new InvariantViolation("Introspection result missing possibleTypes: {$safeUnion}.");
- }
-
- return new UnionType([
- 'name' => $union['name'],
- 'description' => $union['description'],
- 'types' => fn (): array => array_map(
- [$this, 'getObjectType'],
- $union['possibleTypes']
- ),
- ]);
- }
-
- /**
- * @param array<string, mixed> $enum
- *
- * @throws InvariantViolation
- */
- private function buildEnumDef(array $enum): EnumType
- {
- if (! array_key_exists('enumValues', $enum)) {
- $safeEnum = Utils::printSafeJson($enum);
- throw new InvariantViolation("Introspection result missing enumValues: {$safeEnum}.");
- }
-
- $values = [];
- foreach ($enum['enumValues'] as $value) {
- $values[$value['name']] = [
- 'description' => $value['description'],
- 'deprecationReason' => $value['deprecationReason'],
- ];
- }
-
- return new EnumType([
- 'name' => $enum['name'],
- 'description' => $enum['description'],
- 'values' => $values,
- ]);
- }
-
- /**
- * @param array<string, mixed> $inputObject
- *
- * @throws InvariantViolation
- */
- private function buildInputObjectDef(array $inputObject): InputObjectType
- {
- if (! array_key_exists('inputFields', $inputObject)) {
- $safeInputObject = Utils::printSafeJson($inputObject);
- throw new InvariantViolation("Introspection result missing inputFields: {$safeInputObject}.");
- }
-
- return new InputObjectType([
- 'name' => $inputObject['name'],
- 'description' => $inputObject['description'],
- 'fields' => fn (): array => $this->buildInputValueDefMap($inputObject['inputFields']),
- ]);
- }
-
- /**
- * @param array<string, mixed> $typeIntrospection
- *
- * @throws \Exception
- * @throws InvariantViolation
- *
- * @return array<string, UnnamedFieldDefinitionConfig>
- */
- private function buildFieldDefMap(array $typeIntrospection): array
- {
- if (! array_key_exists('fields', $typeIntrospection)) {
- $safeType = Utils::printSafeJson($typeIntrospection);
- throw new InvariantViolation("Introspection result missing fields: {$safeType}.");
- }
-
- /** @var array<string, UnnamedFieldDefinitionConfig> $map */
- $map = [];
- foreach ($typeIntrospection['fields'] as $field) {
- if (! array_key_exists('args', $field)) {
- $safeField = Utils::printSafeJson($field);
- throw new InvariantViolation("Introspection result missing field args: {$safeField}.");
- }
-
- $map[$field['name']] = [
- 'description' => $field['description'],
- 'deprecationReason' => $field['deprecationReason'],
- 'type' => $this->getOutputType($field['type']),
- 'args' => $this->buildInputValueDefMap($field['args']),
- ];
- }
-
- // @phpstan-ignore-next-line unless the returned name was numeric, this works
- return $map;
- }
-
- /**
- * @param array<int, array<string, mixed>> $inputValueIntrospections
- *
- * @throws \Exception
- *
- * @return array<string, UnnamedInputObjectFieldConfig>
- */
- private function buildInputValueDefMap(array $inputValueIntrospections): array
- {
- /** @var array<string, UnnamedInputObjectFieldConfig> $map */
- $map = [];
- foreach ($inputValueIntrospections as $value) {
- $map[$value['name']] = $this->buildInputValue($value);
- }
-
- return $map;
- }
-
- /**
- * @param array<string, mixed> $inputValueIntrospection
- *
- * @throws \Exception
- * @throws SyntaxError
- *
- * @return UnnamedInputObjectFieldConfig
- */
- public function buildInputValue(array $inputValueIntrospection): array
- {
- $type = $this->getInputType($inputValueIntrospection['type']);
-
- $inputValue = [
- 'description' => $inputValueIntrospection['description'],
- 'type' => $type,
- ];
-
- if (isset($inputValueIntrospection['defaultValue'])) {
- $inputValue['defaultValue'] = AST::valueFromAST(
- Parser::parseValue($inputValueIntrospection['defaultValue']),
- $type
- );
- }
-
- return $inputValue;
- }
-
- /**
- * @param array<string, mixed> $directive
- *
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function buildDirective(array $directive): Directive
- {
- if (! array_key_exists('args', $directive)) {
- $safeDirective = Utils::printSafeJson($directive);
- throw new InvariantViolation("Introspection result missing directive args: {$safeDirective}.");
- }
-
- if (! array_key_exists('locations', $directive)) {
- $safeDirective = Utils::printSafeJson($directive);
- throw new InvariantViolation("Introspection result missing directive locations: {$safeDirective}.");
- }
-
- return new Directive([
- 'name' => $directive['name'],
- 'description' => $directive['description'],
- 'args' => $this->buildInputValueDefMap($directive['args']),
- 'isRepeatable' => $directive['isRepeatable'] ?? false,
- 'locations' => $directive['locations'],
- ]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php
deleted file mode 100644
index 707579a1d1c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php
+++ /dev/null
@@ -1,282 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-
-/**
- * Build instance of @see \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema out of schema language definition (string or parsed AST).
- *
- * See [schema definition language docs](schema-definition-language.md) for details.
- *
- * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder
- * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder
- *
- * @phpstan-type BuildSchemaOptions array{
- * assumeValid?: bool,
- * assumeValidSDL?: bool
- * }
- *
- * - assumeValid:
- * When building a schema from a Automattic\WooCommerce\Vendor\GraphQL service's introspection result, it might be safe to assume the schema is valid.
- * Set to true to assume the produced schema is valid.
- * Default: false
- *
- * - assumeValidSDL:
- * Set to true to assume the SDL is valid.
- * Default: false
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BuildSchemaTest
- */
-class BuildSchema
-{
- private DocumentNode $ast;
-
- /**
- * @var callable|null
- *
- * @phpstan-var TypeConfigDecorator|null
- */
- private $typeConfigDecorator;
-
- /**
- * @var callable|null
- *
- * @phpstan-var FieldConfigDecorator|null
- */
- private $fieldConfigDecorator;
-
- /**
- * @var array<string, bool>
- *
- * @phpstan-var BuildSchemaOptions
- */
- private array $options;
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- * @phpstan-param BuildSchemaOptions $options
- */
- public function __construct(
- DocumentNode $ast,
- ?callable $typeConfigDecorator = null,
- array $options = [],
- ?callable $fieldConfigDecorator = null
- ) {
- $this->ast = $ast;
- $this->typeConfigDecorator = $typeConfigDecorator;
- $this->options = $options;
- $this->fieldConfigDecorator = $fieldConfigDecorator;
- }
-
- /**
- * A helper function to build a GraphQLSchema directly from a source
- * document.
- *
- * @param DocumentNode|Source|string $source
- * @param array<string, bool> $options
- *
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
- * @phpstan-param BuildSchemaOptions $options
- *
- * @api
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- * @throws SyntaxError
- */
- public static function build(
- $source,
- ?callable $typeConfigDecorator = null,
- array $options = [],
- ?callable $fieldConfigDecorator = null
- ): Schema {
- $doc = $source instanceof DocumentNode
- ? $source
- : Parser::parse($source);
-
- return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator);
- }
-
- /**
- * This takes the AST of a schema from @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Parser::parse().
- *
- * If no schema definition is provided, then it will look for types named Query and Mutation.
- *
- * Given that AST it constructs a @see \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema. The resulting schema
- * has no resolve methods, so execution will use default resolvers.
- *
- * @param array<string, bool> $options
- *
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
- * @phpstan-param BuildSchemaOptions $options
- *
- * @api
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- */
- public static function buildAST(
- DocumentNode $ast,
- ?callable $typeConfigDecorator = null,
- array $options = [],
- ?callable $fieldConfigDecorator = null
- ): Schema {
- return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator))->buildSchema();
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- */
- public function buildSchema(): Schema
- {
- if (
- ! ($this->options['assumeValid'] ?? false)
- && ! ($this->options['assumeValidSDL'] ?? false)
- ) {
- DocumentValidator::assertValidSDL($this->ast);
- }
-
- $schemaDef = null;
-
- /** @var array<string, Node&TypeDefinitionNode> */
- $typeDefinitionsMap = [];
-
- /** @var array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap */
- $typeExtensionsMap = [];
-
- /** @var array<int, DirectiveDefinitionNode> $directiveDefs */
- $directiveDefs = [];
-
- foreach ($this->ast->definitions as $definition) {
- switch (true) {
- case $definition instanceof SchemaDefinitionNode:
- $schemaDef = $definition;
- break;
- case $definition instanceof TypeDefinitionNode:
- $name = $definition->getName()->value;
- $typeDefinitionsMap[$name] = $definition;
- break;
- case $definition instanceof TypeExtensionNode:
- $name = $definition->getName()->value;
- $typeExtensionsMap[$name][] = $definition;
- break;
- case $definition instanceof DirectiveDefinitionNode:
- $directiveDefs[] = $definition;
- break;
- }
- }
-
- $operationTypes = $schemaDef !== null
- ? $this->getOperationTypes($schemaDef)
- : [
- 'query' => 'Query',
- 'mutation' => 'Mutation',
- 'subscription' => 'Subscription',
- ];
-
- $definitionBuilder = new ASTDefinitionBuilder(
- $typeDefinitionsMap,
- $typeExtensionsMap,
- static function (string $typeName): Type {
- throw self::unknownType($typeName);
- },
- $this->typeConfigDecorator,
- $this->fieldConfigDecorator
- );
-
- $directives = array_map(
- [$definitionBuilder, 'buildDirective'],
- $directiveDefs
- );
-
- $directivesByName = [];
- foreach ($directives as $directive) {
- $directivesByName[$directive->name][] = $directive;
- }
-
- // If specified directives were not explicitly declared, add them.
- if (! isset($directivesByName['include'])) {
- $directives[] = Directive::includeDirective();
- }
- if (! isset($directivesByName['skip'])) {
- $directives[] = Directive::skipDirective();
- }
- if (! isset($directivesByName['deprecated'])) {
- $directives[] = Directive::deprecatedDirective();
- }
- if (! isset($directivesByName['oneOf'])) {
- $directives[] = Directive::oneOfDirective();
- }
-
- // Note: While this could make early assertions to get the correctly
- // typed values below, that would throw immediately while type system
- // validation with validateSchema() will produce more actionable results.
- return new Schema(
- (new SchemaConfig())
- ->setDescription($schemaDef->description->value ?? null)
- // @phpstan-ignore-next-line
- ->setQuery(isset($operationTypes['query'])
- ? $definitionBuilder->maybeBuildType($operationTypes['query'])
- : null)
- // @phpstan-ignore-next-line
- ->setMutation(isset($operationTypes['mutation'])
- ? $definitionBuilder->maybeBuildType($operationTypes['mutation'])
- : null)
- // @phpstan-ignore-next-line
- ->setSubscription(isset($operationTypes['subscription'])
- ? $definitionBuilder->maybeBuildType($operationTypes['subscription'])
- : null)
- ->setTypeLoader(static fn (string $name): ?Type => $definitionBuilder->maybeBuildType($name))
- ->setDirectives($directives)
- ->setAstNode($schemaDef)
- ->setTypes(fn (): array => array_map(
- static fn (TypeDefinitionNode $def): Type => $definitionBuilder->buildType($def->getName()->value),
- $typeDefinitionsMap,
- ))
- );
- }
-
- /** @return array<string, string> */
- private function getOperationTypes(SchemaDefinitionNode $schemaDef): array
- {
- /** @var array<string, string> $operationTypes */
- $operationTypes = [];
- foreach ($schemaDef->operationTypes as $operationType) {
- $operationTypes[$operationType->operation] = $operationType->type->name->value;
- }
-
- return $operationTypes;
- }
-
- public static function unknownType(string $typeName): Error
- {
- return new Error("Unknown type: \"{$typeName}\".");
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php
deleted file mode 100644
index 2ad53929629..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-
-/**
- * A way to track interface implementations.
- *
- * Distinguishes between implementations by ObjectTypes and InterfaceTypes.
- */
-class InterfaceImplementations
-{
- /** @var array<int, ObjectType> */
- private $objects;
-
- /** @var array<int, InterfaceType> */
- private $interfaces;
-
- /**
- * @param array<int, ObjectType> $objects
- * @param array<int, InterfaceType> $interfaces
- */
- public function __construct(array $objects, array $interfaces)
- {
- $this->objects = $objects;
- $this->interfaces = $interfaces;
- }
-
- /** @return array<int, ObjectType> */
- public function objects(): array
- {
- return $this->objects;
- }
-
- /** @return array<int, InterfaceType> */
- public function interfaces(): array
- {
- return $this->interfaces;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php
deleted file mode 100644
index 3e91265b236..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-/**
- * Allows lazy calculation of a complex message when the exception is used in `assert()`.
- */
-class LazyException extends \Exception
-{
- /** @param callable(): string $makeMessage */
- public function __construct(callable $makeMessage)
- {
- parent::__construct($makeMessage());
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php
deleted file mode 100644
index 20e17015010..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-/**
- * Computes the lexical distance between strings A and B.
- *
- * The "distance" between two strings is given by counting the minimum number
- * of edits needed to transform string A into string B. An edit can be an
- * insertion, deletion, or substitution of a single character, or a swap of two
- * adjacent characters.
- *
- * Includes a custom alteration from Damerau-Levenshtein to treat case changes
- * as a single edit which helps identify mis-cased values with an edit distance
- * of 1.
- *
- * This distance can be useful for detecting typos in input or sorting
- *
- * Unlike the native levenshtein() function that always returns int, LexicalDistance::measure() returns int|null.
- * It takes into account the threshold and returns null if the measured distance is bigger.
- */
-class LexicalDistance
-{
- private string $input;
-
- private string $inputLowerCase;
-
- /**
- * List of char codes in the input string.
- *
- * @var array<int>
- */
- private array $inputArray;
-
- public function __construct(string $input)
- {
- $this->input = $input;
- $this->inputLowerCase = strtolower($input);
- $this->inputArray = self::stringToArray($this->inputLowerCase);
- }
-
- public function measure(string $option, float $threshold): ?int
- {
- if ($this->input === $option) {
- return 0;
- }
-
- $optionLowerCase = strtolower($option);
-
- // Any case change counts as a single edit
- if ($this->inputLowerCase === $optionLowerCase) {
- return 1;
- }
-
- $a = self::stringToArray($optionLowerCase);
- $b = $this->inputArray;
-
- if (count($a) < count($b)) {
- $tmp = $a;
- $a = $b;
- $b = $tmp;
- }
-
- $aLength = count($a);
- $bLength = count($b);
-
- if ($aLength - $bLength > $threshold) {
- return null;
- }
-
- /** @var array<array<int>> $rows */
- $rows = [];
- for ($i = 0; $i <= $bLength; ++$i) {
- $rows[0][$i] = $i;
- }
-
- for ($i = 1; $i <= $aLength; ++$i) {
- $upRow = &$rows[($i - 1) % 3];
- $currentRow = &$rows[$i % 3];
-
- $smallestCell = ($currentRow[0] = $i);
- for ($j = 1; $j <= $bLength; ++$j) {
- $cost = $a[$i - 1] === $b[$j - 1] ? 0 : 1;
-
- $currentCell = min(
- $upRow[$j] + 1, // delete
- $currentRow[$j - 1] + 1, // insert
- $upRow[$j - 1] + $cost, // substitute
- );
-
- if ($i > 1 && $j > 1 && $a[$i - 1] === $b[$j - 2] && $a[$i - 2] === $b[$j - 1]) {
- // transposition
- $doubleDiagonalCell = $rows[($i - 2) % 3][$j - 2];
- $currentCell = min($currentCell, $doubleDiagonalCell + 1);
- }
-
- if ($currentCell < $smallestCell) {
- $smallestCell = $currentCell;
- }
-
- $currentRow[$j] = $currentCell;
- }
-
- // Early exit, since distance can't go smaller than smallest element of the previous row.
- if ($smallestCell > $threshold) {
- return null;
- }
- }
-
- $distance = $rows[$aLength % 3][$bLength];
-
- return $distance <= $threshold ? $distance : null;
- }
-
- /**
- * Returns a list of char codes in the given string.
- *
- * @return array<int>
- */
- private static function stringToArray(string $str): array
- {
- $array = [];
- foreach (mb_str_split($str) as $char) {
- $array[] = mb_ord($char);
- }
-
- return $array;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php
deleted file mode 100644
index a3200b0e739..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php
+++ /dev/null
@@ -1,210 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-/**
- * Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars).
- *
- * When storing array as key, access and modification is O(N). Avoid if possible.
- *
- * @template TValue of mixed
- *
- * @implements \ArrayAccess<mixed, TValue>
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\MixedStoreTest
- */
-class MixedStore implements \ArrayAccess
-{
- /** @var array<TValue> */
- private array $standardStore = [];
-
- /** @var array<TValue> */
- private array $floatStore = [];
-
- /** @var \SplObjectStorage<object, TValue> */
- private \SplObjectStorage $objectStore;
-
- /** @var array<int, array<mixed>> */
- private array $arrayKeys = [];
-
- /** @var array<int, TValue> */
- private array $arrayValues = [];
-
- /** @var array<mixed> */
- private ?array $lastArrayKey = null;
-
- /** @var TValue|null */
- private $lastArrayValue;
-
- /** @var TValue|null */
- private $nullValue;
-
- private bool $nullValueIsSet = false;
-
- /** @var TValue|null */
- private $trueValue;
-
- private bool $trueValueIsSet = false;
-
- /** @var TValue|null */
- private $falseValue;
-
- private bool $falseValueIsSet = false;
-
- public function __construct()
- {
- $this->objectStore = new \SplObjectStorage();
- }
-
- /** @param mixed $offset */
- #[\ReturnTypeWillChange]
- public function offsetExists($offset): bool
- {
- if ($offset === false) {
- return $this->falseValueIsSet;
- }
-
- if ($offset === true) {
- return $this->trueValueIsSet;
- }
-
- if (is_int($offset) || is_string($offset)) {
- return array_key_exists($offset, $this->standardStore);
- }
-
- if (is_float($offset)) {
- return array_key_exists((string) $offset, $this->floatStore);
- }
-
- if (is_object($offset)) {
- return $this->objectStore->offsetExists($offset);
- }
-
- if (is_array($offset)) {
- foreach ($this->arrayKeys as $index => $entry) {
- if ($entry === $offset) {
- $this->lastArrayKey = $offset;
- $this->lastArrayValue = $this->arrayValues[$index];
-
- return true;
- }
- }
- }
-
- if ($offset === null) {
- return $this->nullValueIsSet;
- }
-
- return false;
- }
-
- /**
- * @param mixed $offset
- *
- * @return TValue|null
- */
- #[\ReturnTypeWillChange]
- public function offsetGet($offset)
- {
- if ($offset === true) {
- return $this->trueValue;
- }
-
- if ($offset === false) {
- return $this->falseValue;
- }
-
- if (is_int($offset) || is_string($offset)) {
- return $this->standardStore[$offset];
- }
-
- if (is_float($offset)) {
- return $this->floatStore[(string) $offset];
- }
-
- if (is_object($offset)) {
- return $this->objectStore->offsetGet($offset);
- }
-
- if (is_array($offset)) {
- // offsetGet is often called directly after offsetExists, so optimize to avoid second loop:
- if ($this->lastArrayKey === $offset) {
- return $this->lastArrayValue;
- }
-
- foreach ($this->arrayKeys as $index => $entry) {
- if ($entry === $offset) {
- return $this->arrayValues[$index];
- }
- }
- }
-
- if ($offset === null) {
- return $this->nullValue;
- }
-
- return null;
- }
-
- /**
- * @param mixed $offset
- * @param TValue $value
- *
- * @throws \InvalidArgumentException
- */
- #[\ReturnTypeWillChange]
- public function offsetSet($offset, $value): void
- {
- if ($offset === false) {
- $this->falseValue = $value;
- $this->falseValueIsSet = true;
- } elseif ($offset === true) {
- $this->trueValue = $value;
- $this->trueValueIsSet = true;
- } elseif (is_int($offset) || is_string($offset)) {
- $this->standardStore[$offset] = $value;
- } elseif (is_float($offset)) {
- $this->floatStore[(string) $offset] = $value;
- } elseif (is_object($offset)) {
- $this->objectStore[$offset] = $value;
- } elseif (is_array($offset)) {
- $this->arrayKeys[] = $offset;
- $this->arrayValues[] = $value;
- } elseif ($offset === null) {
- $this->nullValue = $value;
- $this->nullValueIsSet = true;
- } else {
- $unexpectedOffset = Utils::printSafe($offset);
- throw new \InvalidArgumentException("Unexpected offset type: {$unexpectedOffset}");
- }
- }
-
- /** @param mixed $offset */
- #[\ReturnTypeWillChange]
- public function offsetUnset($offset): void
- {
- if ($offset === true) {
- $this->trueValue = null;
- $this->trueValueIsSet = false;
- } elseif ($offset === false) {
- $this->falseValue = null;
- $this->falseValueIsSet = false;
- } elseif (is_int($offset) || is_string($offset)) {
- unset($this->standardStore[$offset]);
- } elseif (is_float($offset)) {
- unset($this->floatStore[(string) $offset]);
- } elseif (is_object($offset)) {
- $this->objectStore->offsetUnset($offset);
- } elseif (is_array($offset)) {
- $index = array_search($offset, $this->arrayKeys, true);
-
- if ($index !== false) {
- array_splice($this->arrayKeys, $index, 1);
- array_splice($this->arrayValues, $index, 1);
- }
- } elseif ($offset === null) {
- $this->nullValue = null;
- $this->nullValueIsSet = false;
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php
deleted file mode 100644
index 2268037a9d5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-/**
- * A way to keep track of pairs of things when the ordering of the pair does
- * not matter. We do this by maintaining a sort of double adjacency sets.
- */
-class PairSet
-{
- /** @var array<string, array<string, bool>> */
- private array $data = [];
-
- public function has(string $a, string $b, bool $areMutuallyExclusive): bool
- {
- $first = $this->data[$a] ?? null;
- $result = $first !== null && isset($first[$b]) ? $first[$b] : null;
- if ($result === null) {
- return false;
- }
-
- // areMutuallyExclusive being false is a superset of being true,
- // hence if we want to know if this PairSet "has" these two with no
- // exclusivity, we have to ensure it was added as such.
- if ($areMutuallyExclusive === false) {
- return $result === false;
- }
-
- return true;
- }
-
- public function add(string $a, string $b, bool $areMutuallyExclusive): void
- {
- $this->pairSetAdd($a, $b, $areMutuallyExclusive);
- $this->pairSetAdd($b, $a, $areMutuallyExclusive);
- }
-
- private function pairSetAdd(string $a, string $b, bool $areMutuallyExclusive): void
- {
- $this->data[$a] ??= [];
- $this->data[$a][$b] = $areMutuallyExclusive;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php
deleted file mode 100644
index 620620d9642..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-class PhpDoc
-{
- /** @param string|false|null $docBlock */
- public static function unwrap($docBlock): ?string
- {
- if ($docBlock === false || $docBlock === null) {
- return null;
- }
-
- $content = preg_replace('~([\r\n]) \* (.*)~i', '$1$2', $docBlock); // strip *
- assert(is_string($content), 'regex is statically known to be valid');
-
- $content = preg_replace('~([\r\n])[\* ]+([\r\n])~i', '$1$2', $content); // strip single-liner *
- assert(is_string($content), 'regex is statically known to be valid');
-
- $content = substr($content, 3); // strip leading /**
- $content = substr($content, 0, -2); // strip trailing */
-
- return static::nonEmptyOrNull($content);
- }
-
- /** @param string|false|null $docBlock */
- public static function unpad($docBlock): ?string
- {
- if ($docBlock === false || $docBlock === null) {
- return null;
- }
-
- $lines = explode("\n", $docBlock);
- $lines = array_map(
- static fn (string $line): string => ' ' . trim($line),
- $lines
- );
-
- $content = implode("\n", $lines);
-
- return static::nonEmptyOrNull($content);
- }
-
- protected static function nonEmptyOrNull(string $maybeEmptyString): ?string
- {
- $trimmed = trim($maybeEmptyString);
-
- return $trimmed === ''
- ? null
- : $trimmed;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php
deleted file mode 100644
index 2e11bbf9eda..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php
+++ /dev/null
@@ -1,669 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-
-/**
- * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder
- * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder
- * @phpstan-import-type UnnamedArgumentConfig from Argument
- * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\SchemaExtenderTest
- */
-class SchemaExtender
-{
- /** @var array<string, Type> */
- protected array $extendTypeCache = [];
-
- /** @var array<string, array<TypeExtensionNode>> */
- protected array $typeExtensionsMap = [];
-
- protected ASTDefinitionBuilder $astBuilder;
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
- *
- * @api
- *
- * @throws \Exception
- * @throws InvariantViolation
- */
- public static function extend(
- Schema $schema,
- DocumentNode $documentAST,
- array $options = [],
- ?callable $typeConfigDecorator = null,
- ?callable $fieldConfigDecorator = null
- ): Schema {
- return (new static())->doExtend($schema, $documentAST, $options, $typeConfigDecorator, $fieldConfigDecorator);
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
- * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- */
- protected function doExtend(
- Schema $schema,
- DocumentNode $documentAST,
- array $options = [],
- ?callable $typeConfigDecorator = null,
- ?callable $fieldConfigDecorator = null
- ): Schema {
- if (
- ! ($options['assumeValid'] ?? false)
- && ! ($options['assumeValidSDL'] ?? false)
- ) {
- DocumentValidator::assertValidSDLExtension($documentAST, $schema);
- }
-
- /** @var array<string, Node&TypeDefinitionNode> $typeDefinitionMap */
- $typeDefinitionMap = [];
-
- /** @var array<int, DirectiveDefinitionNode> $directiveDefinitions */
- $directiveDefinitions = [];
-
- /** @var SchemaDefinitionNode|null $schemaDef */
- $schemaDef = null;
-
- /** @var array<int, SchemaExtensionNode> $schemaExtensions */
- $schemaExtensions = [];
-
- foreach ($documentAST->definitions as $def) {
- if ($def instanceof SchemaDefinitionNode) {
- $schemaDef = $def;
- } elseif ($def instanceof SchemaExtensionNode) {
- $schemaExtensions[] = $def;
- } elseif ($def instanceof TypeDefinitionNode) {
- $name = $def->getName()->value;
- $typeDefinitionMap[$name] = $def;
- } elseif ($def instanceof TypeExtensionNode) {
- $name = $def->getName()->value;
- $this->typeExtensionsMap[$name][] = $def;
- } elseif ($def instanceof DirectiveDefinitionNode) {
- $directiveDefinitions[] = $def;
- }
- }
-
- if (
- $this->typeExtensionsMap === []
- && $typeDefinitionMap === []
- && $directiveDefinitions === []
- && $schemaExtensions === []
- && $schemaDef === null
- ) {
- return $schema;
- }
-
- $this->astBuilder = new ASTDefinitionBuilder(
- $typeDefinitionMap,
- [],
- function (string $typeName) use ($schema): Type {
- $existingType = $schema->getType($typeName);
- if ($existingType === null) {
- throw new InvariantViolation("Unknown type: \"{$typeName}\".");
- }
-
- return $this->extendNamedType($existingType);
- },
- $typeConfigDecorator,
- $fieldConfigDecorator
- );
-
- $this->extendTypeCache = [];
-
- $types = [];
-
- // Iterate through all types, getting the type definition for each, ensuring
- // that any type not directly referenced by a field will get created.
- foreach ($schema->getTypeMap() as $type) {
- $types[] = $this->extendNamedType($type);
- }
-
- // Do the same with new types.
- foreach ($typeDefinitionMap as $type) {
- $types[] = $this->astBuilder->buildType($type);
- }
-
- $operationTypes = [
- 'query' => $this->extendMaybeNamedType($schema->getQueryType()),
- 'mutation' => $this->extendMaybeNamedType($schema->getMutationType()),
- 'subscription' => $this->extendMaybeNamedType($schema->getSubscriptionType()),
- ];
-
- if ($schemaDef !== null) {
- foreach ($schemaDef->operationTypes as $operationType) {
- $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type);
- }
- }
-
- foreach ($schemaExtensions as $schemaExtension) {
- foreach ($schemaExtension->operationTypes as $operationType) {
- $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type);
- }
- }
-
- $schemaConfig = (new SchemaConfig())
- ->setDescription($schemaDef->description->value ?? $schema->description ?? null)
- // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
- ->setQuery($operationTypes['query'])
- // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
- ->setMutation($operationTypes['mutation'])
- // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
- ->setSubscription($operationTypes['subscription'])
- ->setTypes($types)
- ->setDirectives($this->getMergedDirectives($schema, $directiveDefinitions))
- ->setAstNode($schema->astNode ?? $schemaDef)
- ->setExtensionASTNodes([...$schema->extensionASTNodes, ...$schemaExtensions]);
-
- return new Schema($schemaConfig);
- }
-
- /**
- * @param Type&NamedType $type
- *
- * @return array<TypeExtensionNode>|null
- */
- protected function extensionASTNodes(NamedType $type): ?array
- {
- return [
- ...$type->extensionASTNodes ?? [],
- ...$this->typeExtensionsMap[$type->name] ?? [],
- ];
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- protected function extendScalarType(ScalarType $type): CustomScalarType
- {
- /** @var array<ScalarTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new CustomScalarType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'serialize' => [$type, 'serialize'],
- 'parseValue' => [$type, 'parseValue'],
- 'parseLiteral' => [$type, 'parseLiteral'],
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /** @throws InvariantViolation */
- protected function extendUnionType(UnionType $type): UnionType
- {
- /** @var array<UnionTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new UnionType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'types' => fn (): array => $this->extendUnionPossibleTypes($type),
- 'resolveType' => [$type, 'resolveType'],
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- protected function extendEnumType(EnumType $type): EnumType
- {
- /** @var array<EnumTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new EnumType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'values' => $this->extendEnumValueMap($type),
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /** @throws InvariantViolation */
- protected function extendInputObjectType(InputObjectType $type): InputObjectType
- {
- /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new InputObjectType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'fields' => fn (): array => $this->extendInputFieldMap($type),
- 'parseValue' => [$type, 'parseValue'],
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- 'isOneOf' => $type->isOneOf,
- ]);
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- *
- * @return array<string, UnnamedInputObjectFieldConfig>
- */
- protected function extendInputFieldMap(InputObjectType $type): array
- {
- /** @var array<string, UnnamedInputObjectFieldConfig> $newFieldMap */
- $newFieldMap = [];
-
- $oldFieldMap = $type->getFields();
- foreach ($oldFieldMap as $fieldName => $field) {
- $extendedType = $this->extendType($field->getType());
-
- $newFieldConfig = [
- 'description' => $field->description,
- 'type' => $extendedType,
- 'deprecationReason' => $field->deprecationReason,
- 'astNode' => $field->astNode,
- ];
-
- if ($field->defaultValueExists()) {
- $newFieldConfig['defaultValue'] = $field->defaultValue;
- }
-
- $newFieldMap[$fieldName] = $newFieldConfig;
- }
-
- if (isset($this->typeExtensionsMap[$type->name])) {
- foreach ($this->typeExtensionsMap[$type->name] as $extension) {
- assert($extension instanceof InputObjectTypeExtensionNode, 'proven by schema validation');
-
- foreach ($extension->fields as $field) {
- $newFieldMap[$field->name->value] = $this->astBuilder->buildInputField($field);
- }
- }
- }
-
- return $newFieldMap;
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- *
- * @return array<string, array<string, mixed>>
- */
- protected function extendEnumValueMap(EnumType $type): array
- {
- $newValueMap = [];
-
- foreach ($type->getValues() as $value) {
- $newValueMap[$value->name] = [
- 'name' => $value->name,
- 'description' => $value->description,
- 'value' => $value->value,
- 'deprecationReason' => $value->deprecationReason,
- 'astNode' => $value->astNode,
- ];
- }
-
- if (isset($this->typeExtensionsMap[$type->name])) {
- foreach ($this->typeExtensionsMap[$type->name] as $extension) {
- assert($extension instanceof EnumTypeExtensionNode, 'proven by schema validation');
-
- foreach ($extension->values as $value) {
- $newValueMap[$value->name->value] = $this->astBuilder->buildEnumValue($value);
- }
- }
- }
-
- return $newValueMap;
- }
-
- /**
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<int, ObjectType>
- */
- protected function extendUnionPossibleTypes(UnionType $type): array
- {
- $possibleTypes = array_map(
- [$this, 'extendNamedType'],
- $type->getTypes()
- );
-
- if (isset($this->typeExtensionsMap[$type->name])) {
- foreach ($this->typeExtensionsMap[$type->name] as $extension) {
- assert($extension instanceof UnionTypeExtensionNode, 'proven by schema validation');
-
- foreach ($extension->types as $namedType) {
- $possibleTypes[] = $this->astBuilder->buildType($namedType);
- }
- }
- }
-
- // @phpstan-ignore-next-line proven by schema validation
- return $possibleTypes;
- }
-
- /**
- * @param ObjectType|InterfaceType $type
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<int, InterfaceType>
- */
- protected function extendImplementedInterfaces(ImplementingType $type): array
- {
- $interfaces = array_map(
- [$this, 'extendNamedType'],
- $type->getInterfaces()
- );
-
- if (isset($this->typeExtensionsMap[$type->name])) {
- foreach ($this->typeExtensionsMap[$type->name] as $extension) {
- assert(
- $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
- 'proven by schema validation'
- );
-
- foreach ($extension->interfaces as $namedType) {
- $interface = $this->astBuilder->buildType($namedType);
- assert($interface instanceof InterfaceType, 'we know this, but PHP templates cannot express it');
-
- $interfaces[] = $interface;
- }
- }
- }
-
- return $interfaces;
- }
-
- /**
- * @template T of Type
- *
- * @param T $typeDef
- *
- * @return T
- */
- protected function extendType(Type $typeDef): Type
- {
- if ($typeDef instanceof ListOfType) {
- // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
- return Type::listOf($this->extendType($typeDef->getWrappedType()));
- }
-
- if ($typeDef instanceof NonNull) {
- // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
- return Type::nonNull($this->extendType($typeDef->getWrappedType()));
- }
-
- // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
- return $this->extendNamedType($typeDef);
- }
-
- /**
- * @param array<Argument> $args
- *
- * @return array<string, UnnamedArgumentConfig>
- */
- protected function extendArgs(array $args): array
- {
- $extended = [];
- foreach ($args as $arg) {
- $extendedType = $this->extendType($arg->getType());
-
- $def = [
- 'type' => $extendedType,
- 'description' => $arg->description,
- 'deprecationReason' => $arg->deprecationReason,
- 'astNode' => $arg->astNode,
- ];
-
- if ($arg->defaultValueExists()) {
- $def['defaultValue'] = $arg->defaultValue;
- }
-
- $extended[$arg->name] = $def;
- }
-
- return $extended;
- }
-
- /**
- * @param InterfaceType|ObjectType $type
- *
- * @throws \Exception
- * @throws Error
- * @throws InvariantViolation
- *
- * @return array<string, array<string, mixed>>
- */
- protected function extendFieldMap(Type $type): array
- {
- $newFieldMap = [];
- $oldFieldMap = $type->getFields();
-
- foreach (array_keys($oldFieldMap) as $fieldName) {
- $field = $oldFieldMap[$fieldName];
-
- $newFieldMap[$fieldName] = [
- 'name' => $fieldName,
- 'description' => $field->description,
- 'deprecationReason' => $field->deprecationReason,
- 'type' => $this->extendType($field->getType()),
- 'args' => $this->extendArgs($field->args),
- 'resolve' => $field->resolveFn,
- 'argsMapper' => $field->argsMapper,
- 'astNode' => $field->astNode,
- ];
- }
-
- if (isset($this->typeExtensionsMap[$type->name])) {
- foreach ($this->typeExtensionsMap[$type->name] as $extension) {
- assert(
- $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
- 'proven by schema validation'
- );
-
- foreach ($extension->fields as $field) {
- $newFieldMap[$field->name->value] = $this->astBuilder->buildField($field, $extension);
- }
- }
- }
-
- return $newFieldMap;
- }
-
- /** @throws InvariantViolation */
- protected function extendObjectType(ObjectType $type): ObjectType
- {
- /** @var array<ObjectTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new ObjectType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type),
- 'fields' => fn (): array => $this->extendFieldMap($type),
- 'isTypeOf' => [$type, 'isTypeOf'],
- 'resolveField' => $type->resolveFieldFn,
- 'argsMapper' => $type->argsMapper,
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- /** @throws InvariantViolation */
- protected function extendInterfaceType(InterfaceType $type): InterfaceType
- {
- /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes */
- $extensionASTNodes = $this->extensionASTNodes($type);
-
- return new InterfaceType([
- 'name' => $type->name,
- 'description' => $type->description,
- 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type),
- 'fields' => fn (): array => $this->extendFieldMap($type),
- 'resolveType' => [$type, 'resolveType'],
- 'astNode' => $type->astNode,
- 'extensionASTNodes' => $extensionASTNodes,
- ]);
- }
-
- protected function isSpecifiedScalarType(Type $type): bool
- {
- return $type instanceof NamedType
- && in_array($type->name, [
- Type::STRING,
- Type::INT,
- Type::FLOAT,
- Type::BOOLEAN,
- Type::ID,
- ], true);
- }
-
- /**
- * @template T of Type
- *
- * @param T&NamedType $type
- *
- * @throws \ReflectionException
- * @throws InvariantViolation
- *
- * @return T&NamedType
- */
- protected function extendNamedType(Type $type): Type
- {
- if (Introspection::isIntrospectionType($type) || $this->isSpecifiedScalarType($type)) {
- return $type;
- }
-
- // @phpstan-ignore-next-line the subtypes line up
- return $this->extendTypeCache[$type->name] ??= $this->extendNamedTypeWithoutCache($type);
- }
-
- /** @throws \Exception */
- protected function extendNamedTypeWithoutCache(Type $type): Type
- {
- switch (true) {
- case $type instanceof ScalarType: return $this->extendScalarType($type);
- case $type instanceof ObjectType: return $this->extendObjectType($type);
- case $type instanceof InterfaceType: return $this->extendInterfaceType($type);
- case $type instanceof UnionType: return $this->extendUnionType($type);
- case $type instanceof EnumType: return $this->extendEnumType($type);
- case $type instanceof InputObjectType: return $this->extendInputObjectType($type);
- default:
- $unconsideredType = get_class($type);
- throw new \Exception("Unconsidered type: {$unconsideredType}.");
- }
- }
-
- /**
- * @template T of Type
- *
- * @param (T&NamedType)|null $type
- *
- * @throws \ReflectionException
- * @throws InvariantViolation
- *
- * @return (T&NamedType)|null
- */
- protected function extendMaybeNamedType(?Type $type = null): ?Type
- {
- if ($type !== null) {
- return $this->extendNamedType($type);
- }
-
- return null;
- }
-
- /**
- * @param array<DirectiveDefinitionNode> $directiveDefinitions
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- *
- * @return array<int, Directive>
- */
- protected function getMergedDirectives(Schema $schema, array $directiveDefinitions): array
- {
- $directives = array_map(
- [$this, 'extendDirective'],
- $schema->getDirectives()
- );
-
- if ($directives === []) {
- throw new InvariantViolation('Schema must have default directives.');
- }
-
- foreach ($directiveDefinitions as $directive) {
- $directives[] = $this->astBuilder->buildDirective($directive);
- }
-
- return $directives;
- }
-
- protected function extendDirective(Directive $directive): Directive
- {
- return new Directive([
- 'name' => $directive->name,
- 'description' => $directive->description,
- 'locations' => $directive->locations,
- 'args' => $this->extendArgs($directive->args),
- 'isRepeatable' => $directive->isRepeatable,
- 'astNode' => $directive->astNode,
- ]);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php
deleted file mode 100644
index 9594ed86993..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php
+++ /dev/null
@@ -1,579 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\BlockString;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * Prints the contents of a Schema in schema definition language.
- *
- * All sorting options sort alphabetically. If not given or `false`, the original schema definition order will be used.
- *
- * @phpstan-type Options array{
- * sortArguments?: bool,
- * sortEnumValues?: bool,
- * sortFields?: bool,
- * sortInputFields?: bool,
- * sortTypes?: bool,
- * }
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\SchemaPrinterTest
- */
-class SchemaPrinter
-{
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @api
- *
- * @throws \JsonException
- * @throws Error
- * @throws InvariantViolation
- * @throws SerializationError
- */
- public static function doPrint(Schema $schema, array $options = []): string
- {
- return static::printFilteredSchema(
- $schema,
- static fn (Directive $directive): bool => ! Directive::isSpecifiedDirective($directive),
- static fn (NamedType $type): bool => ! $type->isBuiltInType(),
- $options
- );
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @api
- *
- * @throws \JsonException
- * @throws Error
- * @throws InvariantViolation
- * @throws SerializationError
- */
- public static function printIntrospectionSchema(Schema $schema, array $options = []): string
- {
- return static::printFilteredSchema(
- $schema,
- [Directive::class, 'isSpecifiedDirective'],
- [Introspection::class, 'isIntrospectionType'],
- $options
- );
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws Error
- * @throws InvariantViolation
- * @throws SerializationError
- */
- public static function printType(Type $type, array $options = []): string
- {
- if ($type instanceof ScalarType) {
- return static::printScalar($type, $options);
- }
-
- if ($type instanceof ObjectType) {
- return static::printObject($type, $options);
- }
-
- if ($type instanceof InterfaceType) {
- return static::printInterface($type, $options);
- }
-
- if ($type instanceof UnionType) {
- return static::printUnion($type, $options);
- }
-
- if ($type instanceof EnumType) {
- return static::printEnum($type, $options);
- }
-
- if ($type instanceof InputObjectType) {
- return static::printInputObject($type, $options);
- }
-
- $unknownType = Utils::printSafe($type);
- throw new Error("Unknown type: {$unknownType}.");
- }
-
- /**
- * @param callable(Directive $directive): bool $directiveFilter
- * @param callable(Type&NamedType $type): bool $typeFilter
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws Error
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string
- {
- $directives = array_filter($schema->getDirectives(), $directiveFilter);
- $types = array_filter($schema->getTypeMap(), $typeFilter);
-
- if (isset($options['sortTypes']) && $options['sortTypes']) {
- ksort($types);
- }
-
- $elements = [static::printSchemaDefinition($schema)];
-
- foreach ($directives as $directive) {
- $elements[] = static::printDirective($directive, $options);
- }
-
- foreach ($types as $type) {
- $elements[] = static::printType($type, $options);
- }
-
- /** @phpstan-ignore arrayFilter.strict */
- return implode("\n\n", array_filter($elements)) . "\n";
- }
-
- /**
- * @throws \JsonException
- * @throws InvariantViolation
- */
- protected static function printSchemaDefinition(Schema $schema): ?string
- {
- $queryType = $schema->getQueryType();
- $mutationType = $schema->getMutationType();
- $subscriptionType = $schema->getSubscriptionType();
-
- // Special case: When a schema has no root operation types, no valid schema
- // definition can be printed.
- if ($queryType === null && $mutationType === null && $subscriptionType === null) {
- return null;
- }
-
- // Only print a schema definition if there is a description or if it should
- // not be omitted because of having default type names.
- if ($schema->description !== null || ! static::hasDefaultRootOperationTypes($schema)) {
- return static::printDescription([], $schema) . "schema {\n"
- . ($queryType !== null ? " query: {$queryType->name}\n" : '')
- . ($mutationType !== null ? " mutation: {$mutationType->name}\n" : '')
- . ($subscriptionType !== null ? " subscription: {$subscriptionType->name}\n" : '')
- . '}';
- }
-
- return null;
- }
-
- /**
- * Automattic\WooCommerce\Vendor\GraphQL schema define root types for each type of operation. These types are
- * the same as any other type and can be named in any manner, however there is
- * a common naming convention:.
- *
- * ```graphql
- * schema {
- * query: Query
- * mutation: Mutation
- * subscription: Subscription
- * }
- * ```
- *
- * When using this naming convention, the schema description can be omitted.
- * When using this naming convention, the schema description can be omitted so
- * long as these names are only used for operation types.
- *
- * Note however that if any of these default names are used elsewhere in the
- * schema but not as a root operation type, the schema definition must still
- * be printed to avoid ambiguity.
- *
- * @throws InvariantViolation
- */
- protected static function hasDefaultRootOperationTypes(Schema $schema): bool
- {
- return $schema->getQueryType() === $schema->getType('Query')
- && $schema->getMutationType() === $schema->getType('Mutation')
- && $schema->getSubscriptionType() === $schema->getType('Subscription');
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printDirective(Directive $directive, array $options): string
- {
- return static::printDescription($options, $directive)
- . 'directive @' . $directive->name
- . static::printArgs($options, $directive->args)
- . ($directive->isRepeatable ? ' repeatable' : '')
- . ' on ' . implode(' | ', $directive->locations);
- }
-
- /**
- * @param array<string, bool> $options
- * @param (Type&NamedType)|Directive|EnumValueDefinition|Argument|FieldDefinition|InputObjectField|Schema $def
- *
- * @throws \JsonException
- */
- protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string
- {
- $description = $def->description;
- if ($description === null) {
- return '';
- }
-
- $prefix = $indentation !== '' && ! $firstInBlock
- ? "\n{$indentation}"
- : $indentation;
-
- if (count(Utils::splitLines($description)) === 1) {
- $description = json_encode($description, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
- } else {
- $description = BlockString::print($description);
- $description = $indentation !== ''
- ? str_replace("\n", "\n{$indentation}", $description)
- : $description;
- }
-
- return "{$prefix}{$description}\n";
- }
-
- /**
- * @param array<string, bool> $options
- * @param array<int, Argument> $args
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printArgs(array $options, array $args, string $indentation = ''): string
- {
- if ($args === []) {
- return '';
- }
-
- if (isset($options['sortArguments']) && $options['sortArguments']) {
- usort($args, static fn (Argument $left, Argument $right): int => $left->name <=> $right->name);
- }
-
- $allArgsWithoutDescription = true;
- foreach ($args as $arg) {
- $description = $arg->description;
- if ($description !== null && $description !== '') {
- $allArgsWithoutDescription = false;
- break;
- }
- }
-
- if ($allArgsWithoutDescription) {
- return '('
- . implode(
- ', ',
- array_map(
- [static::class, 'printInputValue'],
- $args
- )
- )
- . ')';
- }
-
- $argsStrings = [];
- $firstInBlock = true;
- $previousHasDescription = false;
- foreach ($args as $arg) {
- $hasDescription = $arg->description !== null;
- if ($previousHasDescription && ! $hasDescription) {
- $argsStrings[] = '';
- }
-
- $argsStrings[] = static::printDescription($options, $arg, ' ' . $indentation, $firstInBlock)
- . ' '
- . $indentation
- . static::printInputValue($arg);
- $firstInBlock = false;
- $previousHasDescription = $hasDescription;
- }
-
- return "(\n"
- . implode("\n", $argsStrings)
- . "\n"
- . $indentation
- . ')';
- }
-
- /**
- * @param InputObjectField|Argument $arg
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printInputValue($arg): string
- {
- $argDecl = "{$arg->name}: {$arg->getType()->toString()}";
-
- if ($arg->defaultValueExists()) {
- $defaultValueAST = AST::astFromValue($arg->defaultValue, $arg->getType());
-
- if ($defaultValueAST === null) {
- $inconvertibleDefaultValue = Utils::printSafe($arg->defaultValue);
- throw new InvariantViolation("Unable to convert defaultValue of argument {$arg->name} into AST: {$inconvertibleDefaultValue}.");
- }
-
- $printedDefaultValue = Printer::doPrint($defaultValueAST);
- $argDecl .= " = {$printedDefaultValue}";
- }
-
- return $argDecl . static::printDeprecated($arg);
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- */
- protected static function printScalar(ScalarType $type, array $options): string
- {
- return static::printDescription($options, $type)
- . "scalar {$type->name}";
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printObject(ObjectType $type, array $options): string
- {
- return static::printDescription($options, $type)
- . "type {$type->name}"
- . static::printImplementedInterfaces($type)
- . static::printFields($options, $type);
- }
-
- /**
- * @param array<string, bool> $options
- * @param ObjectType|InterfaceType $type
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printFields(array $options, $type): string
- {
- $fields = [];
- $firstInBlock = true;
- $previousHasDescription = false;
- $fieldDefinitions = $type->getFields();
-
- if (isset($options['sortFields']) && $options['sortFields']) {
- ksort($fieldDefinitions);
- }
-
- foreach ($fieldDefinitions as $f) {
- $hasDescription = $f->description !== null;
- if ($previousHasDescription && ! $hasDescription) {
- $fields[] = '';
- }
-
- $fields[] = static::printDescription($options, $f, ' ', $firstInBlock)
- . ' '
- . $f->name
- . static::printArgs($options, $f->args, ' ')
- . ': '
- . $f->getType()->toString()
- . static::printDeprecated($f);
- $firstInBlock = false;
- $previousHasDescription = $hasDescription;
- }
-
- return static::printBlock($fields);
- }
-
- /**
- * @param FieldDefinition|EnumValueDefinition|InputObjectField|Argument $deprecation
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printDeprecated($deprecation): string
- {
- $reason = $deprecation->deprecationReason;
- if ($reason === null) {
- return '';
- }
-
- if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
- return ' @deprecated';
- }
-
- $reasonAST = AST::astFromValue($reason, Type::string());
- assert($reasonAST instanceof StringValueNode);
-
- $reasonASTString = Printer::doPrint($reasonAST);
-
- return " @deprecated(reason: {$reasonASTString})";
- }
-
- protected static function printImplementedInterfaces(ImplementingType $type): string
- {
- $interfaces = $type->getInterfaces();
-
- return $interfaces === []
- ? ''
- : ' implements ' . implode(
- ' & ',
- array_map(
- static fn (InterfaceType $interface): string => $interface->name,
- $interfaces
- )
- );
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printInterface(InterfaceType $type, array $options): string
- {
- return static::printDescription($options, $type)
- . "interface {$type->name}"
- . static::printImplementedInterfaces($type)
- . static::printFields($options, $type);
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- */
- protected static function printUnion(UnionType $type, array $options): string
- {
- $types = $type->getTypes();
- $types = $types === []
- ? ''
- : ' = ' . implode(' | ', $types);
-
- return static::printDescription($options, $type) . 'union ' . $type->name . $types;
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printEnum(EnumType $type, array $options): string
- {
- $values = [];
- $firstInBlock = true;
- $valueDefinitions = $type->getValues();
-
- if (isset($options['sortEnumValues']) && $options['sortEnumValues']) {
- usort($valueDefinitions, static fn (EnumValueDefinition $left, EnumValueDefinition $right): int => $left->name <=> $right->name);
- }
-
- foreach ($valueDefinitions as $value) {
- $values[] = static::printDescription($options, $value, ' ', $firstInBlock)
- . ' '
- . $value->name
- . static::printDeprecated($value);
- $firstInBlock = false;
- }
-
- return static::printDescription($options, $type)
- . "enum {$type->name}"
- . static::printBlock($values);
- }
-
- /**
- * @param array<string, bool> $options
- *
- * @phpstan-param Options $options
- *
- * @throws \JsonException
- * @throws InvariantViolation
- * @throws SerializationError
- */
- protected static function printInputObject(InputObjectType $type, array $options): string
- {
- $fields = [];
- $firstInBlock = true;
- $fieldDefinitions = $type->getFields();
-
- if (isset($options['sortInputFields']) && $options['sortInputFields']) {
- ksort($fieldDefinitions);
- }
-
- foreach ($fieldDefinitions as $field) {
- $fields[] = static::printDescription($options, $field, ' ', $firstInBlock)
- . ' '
- . static::printInputValue($field);
- $firstInBlock = false;
- }
-
- return static::printDescription($options, $type)
- . "input {$type->name}"
- . ($type->isOneOf() ? ' @oneOf' : '')
- . static::printBlock($fields);
- }
-
- /** @param array<string> $items */
- protected static function printBlock(array $items): string
- {
- return $items === []
- ? ''
- : " {\n" . implode("\n", $items) . "\n}";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php
deleted file mode 100644
index 5532ccc02a4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-class TypeComparators
-{
- /** Provided two types, return true if the types are equal (invariant). */
- public static function isEqualType(Type $typeA, Type $typeB): bool
- {
- // Equivalent types are equal.
- if ($typeA === $typeB) {
- return true;
- }
-
- if (self::areSameBuiltInScalar($typeA, $typeB)) {
- return true;
- }
-
- // If either type is non-null, the other must also be non-null.
- if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
- return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
- }
-
- // If either type is a list, the other must also be a list.
- if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
- return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
- }
-
- // Otherwise the types are not equal.
- return false;
- }
-
- /**
- * Provided a type and a super type, return true if the first type is either
- * equal or a subset of the second super type (covariant).
- *
- * @throws InvariantViolation
- */
- public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType): bool
- {
- // Equivalent type is a valid subtype
- if ($maybeSubType === $superType) {
- return true;
- }
-
- if (self::areSameBuiltInScalar($maybeSubType, $superType)) {
- return true;
- }
-
- // If superType is non-null, maybeSubType must also be nullable.
- if ($superType instanceof NonNull) {
- if ($maybeSubType instanceof NonNull) {
- return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
- }
-
- return false;
- }
-
- if ($maybeSubType instanceof NonNull) {
- // If superType is nullable, maybeSubType may be non-null.
- return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
- }
-
- // If superType type is a list, maybeSubType type must also be a list.
- if ($superType instanceof ListOfType) {
- if ($maybeSubType instanceof ListOfType) {
- return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
- }
-
- return false;
- }
-
- if ($maybeSubType instanceof ListOfType) {
- // If superType is not a list, maybeSubType must also be not a list.
- return false;
- }
-
- if (Type::isAbstractType($superType)) {
- // If superType type is an abstract type, maybeSubType type may be a currently
- // possible object or interface type.
-
- return $maybeSubType instanceof ImplementingType
- && $schema->isSubType($superType, $maybeSubType);
- }
-
- return false;
- }
-
- /**
- * Built-in scalars may exist as different instances when a type loader
- * overrides them. Compare by name to handle this case.
- */
- private static function areSameBuiltInScalar(Type $typeA, Type $typeB): bool
- {
- return Type::isBuiltInScalar($typeA)
- && Type::isBuiltInScalar($typeB)
- && $typeA->name() === $typeB->name();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php
deleted file mode 100644
index 869ad4efe07..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php
+++ /dev/null
@@ -1,431 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\WrappingType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-class TypeInfo
-{
- private Schema $schema;
-
- /** @var array<int, Type|null> */
- private array $typeStack = [];
-
- /** @var array<int, (CompositeType&Type)|null> */
- private array $parentTypeStack = [];
-
- /** @var array<int, (InputType&Type)|null> */
- private array $inputTypeStack = [];
-
- /** @var array<int, FieldDefinition|null> */
- private array $fieldDefStack = [];
-
- /** @var array<int, mixed> */
- private array $defaultValueStack = [];
-
- private ?Directive $directive = null;
-
- private ?Argument $argument = null;
-
- /** @var mixed */
- private $enumValue;
-
- public function __construct(Schema $schema)
- {
- $this->schema = $schema;
- }
-
- /** @return array<int, (CompositeType&Type)|null> */
- public function getParentTypeStack(): array
- {
- return $this->parentTypeStack;
- }
-
- /** @return array<int, FieldDefinition|null> */
- public function getFieldDefStack(): array
- {
- return $this->fieldDefStack;
- }
-
- /**
- * Given root type scans through all fields to find nested types.
- *
- * Returns array where keys are for type name
- * and value contains corresponding type instance.
- *
- * Example output:
- * [
- * 'String' => $instanceOfStringType,
- * 'MyType' => $instanceOfMyType,
- * ...
- * ]
- *
- * @param (Type&NamedType)|(Type&WrappingType) $type
- * @param array<string, Type&NamedType> $typeMap
- *
- * @throws InvariantViolation
- */
- public static function extractTypes(Type $type, array &$typeMap): void
- {
- if ($type instanceof WrappingType) {
- self::extractTypes($type->getInnermostType(), $typeMap);
-
- return;
- }
-
- $name = $type->name;
- assert(is_string($name));
-
- if (isset($typeMap[$name])) {
- if ($typeMap[$name] !== $type) {
- throw new InvariantViolation("Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).");
- }
-
- return;
- }
-
- $typeMap[$name] = $type;
-
- if ($type instanceof UnionType) {
- foreach ($type->getTypes() as $member) {
- self::extractTypes($member, $typeMap);
- }
-
- return;
- }
-
- if ($type instanceof InputObjectType) {
- foreach ($type->getFields() as $field) {
- $fieldType = $field->getType();
- assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType);
- self::extractTypes($fieldType, $typeMap);
- }
-
- return;
- }
-
- if ($type instanceof ImplementingType) {
- foreach ($type->getInterfaces() as $interface) {
- self::extractTypes($interface, $typeMap);
- }
- }
-
- if ($type instanceof HasFieldsType) {
- foreach ($type->getFields() as $field) {
- foreach ($field->args as $arg) {
- $argType = $arg->getType();
- assert($argType instanceof NamedType || $argType instanceof WrappingType);
- self::extractTypes($argType, $typeMap);
- }
-
- $fieldType = $field->getType();
- assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType);
- self::extractTypes($fieldType, $typeMap);
- }
- }
- }
-
- /**
- * @param array<string, Type&NamedType> $typeMap
- *
- * @throws InvariantViolation
- */
- public static function extractTypesFromDirectives(Directive $directive, array &$typeMap): void
- {
- foreach ($directive->args as $arg) {
- $argType = $arg->getType();
- assert($argType instanceof NamedType || $argType instanceof WrappingType);
- self::extractTypes($argType, $typeMap);
- }
- }
-
- /** @return (Type&InputType)|null */
- public function getParentInputType(): ?InputType
- {
- return $this->inputTypeStack[count($this->inputTypeStack) - 2] ?? null;
- }
-
- public function getArgument(): ?Argument
- {
- return $this->argument;
- }
-
- /** @return mixed */
- public function getEnumValue()
- {
- return $this->enumValue;
- }
-
- /**
- * @throws \Exception
- * @throws InvariantViolation
- */
- public function enter(Node $node): void
- {
- $schema = $this->schema;
-
- // Note: many of the types below are explicitly typed as "mixed" to drop
- // any assumptions of a valid schema to ensure runtime types are properly
- // checked before continuing since TypeInfo is used as part of validation
- // which occurs before guarantees of schema and document validity.
- switch (true) {
- case $node instanceof SelectionSetNode:
- $namedType = Type::getNamedType($this->getType());
- $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null;
- break;
-
- case $node instanceof FieldNode:
- $parentType = $this->getParentType();
-
- $fieldDef = $parentType === null
- ? null
- : self::getFieldDefinition($schema, $parentType, $node);
-
- $fieldType = $fieldDef === null
- ? null
- : $fieldDef->getType();
-
- $this->fieldDefStack[] = $fieldDef;
- $this->typeStack[] = $fieldType;
- break;
-
- case $node instanceof DirectiveNode:
- $this->directive = $schema->getDirective($node->name->value);
- break;
-
- case $node instanceof OperationDefinitionNode:
- if ($node->operation === 'query') {
- $type = $schema->getQueryType();
- } elseif ($node->operation === 'mutation') {
- $type = $schema->getMutationType();
- } else {
- // Only other option
- $type = $schema->getSubscriptionType();
- }
-
- $this->typeStack[] = Type::isOutputType($type)
- ? $type
- : null;
- break;
-
- case $node instanceof InlineFragmentNode:
- case $node instanceof FragmentDefinitionNode:
- $typeConditionNode = $node->typeCondition;
- $outputType = $typeConditionNode === null
- ? Type::getNamedType($this->getType())
- : AST::typeFromAST([$schema, 'getType'], $typeConditionNode);
- $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null;
- break;
-
- case $node instanceof VariableDefinitionNode:
- $inputType = AST::typeFromAST([$schema, 'getType'], $node->type);
- $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push
- break;
-
- case $node instanceof ArgumentNode:
- $fieldOrDirective = $this->getDirective() ?? $this->getFieldDef();
- $argDef = null;
- $argType = null;
- if ($fieldOrDirective !== null) {
- foreach ($fieldOrDirective->args as $arg) {
- if ($arg->name === $node->name->value) {
- $argDef = $arg;
- $argType = $arg->getType();
- }
- }
- }
-
- $this->argument = $argDef;
- $this->defaultValueStack[] = $argDef !== null && $argDef->defaultValueExists()
- ? $argDef->defaultValue
- : Utils::undefined();
- $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
- break;
-
- case $node instanceof ListValueNode:
- $type = $this->getInputType();
- $listType = $type instanceof NonNull
- ? $type->getWrappedType()
- : $type;
- $itemType = $listType instanceof ListOfType
- ? $listType->getWrappedType()
- : $listType;
- // List positions never have a default value.
- $this->defaultValueStack[] = Utils::undefined();
- $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
- break;
-
- case $node instanceof ObjectFieldNode:
- $objectType = Type::getNamedType($this->getInputType());
- $inputField = null;
- $inputFieldType = null;
- if ($objectType instanceof InputObjectType) {
- $tmp = $objectType->getFields();
- $inputField = $tmp[$node->name->value] ?? null;
- $inputFieldType = $inputField === null
- ? null
- : $inputField->getType();
- }
-
- $this->defaultValueStack[] = $inputField !== null && $inputField->defaultValueExists()
- ? $inputField->defaultValue
- : Utils::undefined();
- $this->inputTypeStack[] = Type::isInputType($inputFieldType)
- ? $inputFieldType
- : null;
- break;
-
- case $node instanceof EnumValueNode:
- $enumType = Type::getNamedType($this->getInputType());
-
- $this->enumValue = $enumType instanceof EnumType
- ? $enumType->getValue($node->value)
- : null;
- break;
- }
- }
-
- public function getType(): ?Type
- {
- return $this->typeStack[count($this->typeStack) - 1] ?? null;
- }
-
- /** @return (CompositeType&Type)|null */
- public function getParentType(): ?CompositeType
- {
- return $this->parentTypeStack[count($this->parentTypeStack) - 1] ?? null;
- }
-
- /**
- * Not exactly the same as the executor's definition of getFieldDef, in this
- * statically evaluated environment we do not always have an Object type,
- * and need to handle Interface and Union types.
- *
- * @throws InvariantViolation
- */
- private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition
- {
- $name = $fieldNode->name->value;
- $schemaMeta = Introspection::schemaMetaFieldDef();
- if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) {
- return $schemaMeta;
- }
-
- $typeMeta = Introspection::typeMetaFieldDef();
- if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) {
- return $typeMeta;
- }
-
- $typeNameMeta = Introspection::typeNameMetaFieldDef();
- if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) {
- return $typeNameMeta;
- }
-
- if (
- $parentType instanceof ObjectType
- || $parentType instanceof InterfaceType
- ) {
- return $parentType->findField($name);
- }
-
- return null;
- }
-
- public function getDirective(): ?Directive
- {
- return $this->directive;
- }
-
- public function getFieldDef(): ?FieldDefinition
- {
- return $this->fieldDefStack[count($this->fieldDefStack) - 1] ?? null;
- }
-
- /** @return mixed any value is possible */
- public function getDefaultValue()
- {
- return $this->defaultValueStack[count($this->defaultValueStack) - 1] ?? null;
- }
-
- /** @return (InputType&Type)|null */
- public function getInputType(): ?InputType
- {
- return $this->inputTypeStack[count($this->inputTypeStack) - 1] ?? null;
- }
-
- public function leave(Node $node): void
- {
- switch ($node->kind) {
- case NodeKind::SELECTION_SET:
- array_pop($this->parentTypeStack);
- break;
-
- case NodeKind::FIELD:
- array_pop($this->fieldDefStack);
- array_pop($this->typeStack);
- break;
-
- case NodeKind::DIRECTIVE:
- $this->directive = null;
- break;
-
- case NodeKind::OPERATION_DEFINITION:
- case NodeKind::INLINE_FRAGMENT:
- case NodeKind::FRAGMENT_DEFINITION:
- array_pop($this->typeStack);
- break;
-
- case NodeKind::VARIABLE_DEFINITION:
- array_pop($this->inputTypeStack);
- break;
-
- case NodeKind::ARGUMENT:
- $this->argument = null;
- array_pop($this->defaultValueStack);
- array_pop($this->inputTypeStack);
- break;
-
- case NodeKind::LST:
- case NodeKind::OBJECT_FIELD:
- array_pop($this->defaultValueStack);
- array_pop($this->inputTypeStack);
- break;
-
- case NodeKind::ENUM:
- $this->enumValue = null;
- break;
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php
deleted file mode 100644
index 10fbd1dac9c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php
+++ /dev/null
@@ -1,294 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Warning;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-
-class Utils
-{
- public static function undefined(): \stdClass
- {
- static $undefined;
-
- return $undefined ??= new \stdClass();
- }
-
- /** @param array<string, mixed> $vars */
- public static function assign(object $obj, array $vars): object
- {
- foreach ($vars as $key => $value) {
- if (! property_exists($obj, $key)) {
- $cls = get_class($obj);
- Warning::warn(
- "Trying to set non-existing property '{$key}' on class '{$cls}'",
- Warning::WARNING_ASSIGN
- );
- }
-
- $obj->{$key} = $value;
- }
-
- return $obj;
- }
-
- /**
- * Print a value that came from JSON for debugging purposes.
- *
- * @param mixed $value
- */
- public static function printSafeJson($value): string
- {
- if ($value instanceof \stdClass) {
- return static::jsonEncodeOrSerialize($value);
- }
-
- return static::printSafeInternal($value);
- }
-
- /**
- * Print a value that came from PHP for debugging purposes.
- *
- * @param mixed $value
- */
- public static function printSafe($value): string
- {
- if (is_object($value)) {
- if (method_exists($value, '__toString')) {
- return $value->__toString();
- }
-
- return 'instance of ' . get_class($value);
- }
-
- return static::printSafeInternal($value);
- }
-
- /** @param \stdClass|array<mixed> $value */
- protected static function jsonEncodeOrSerialize($value): string
- {
- try {
- return json_encode($value, JSON_THROW_ON_ERROR);
- } catch (\JsonException $jsonException) {
- return serialize($value);
- }
- }
-
- /** @param mixed $value */
- protected static function printSafeInternal($value): string
- {
- if (is_array($value)) {
- return static::jsonEncodeOrSerialize($value);
- }
-
- if ($value === '') {
- return '(empty string)';
- }
-
- if ($value === null) {
- return 'null';
- }
-
- if ($value === false) {
- return 'false';
- }
-
- if ($value === true) {
- return 'true';
- }
-
- if (is_string($value)) {
- return "\"{$value}\"";
- }
-
- if (is_scalar($value)) {
- return (string) $value;
- }
-
- return gettype($value);
- }
-
- /** UTF-8 compatible chr(). */
- public static function chr(int $ord, string $encoding = 'UTF-8'): string
- {
- if ($encoding === 'UCS-4BE') {
- return pack('N', $ord);
- }
-
- return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
- }
-
- /** UTF-8 compatible ord(). */
- public static function ord(string $char, string $encoding = 'UTF-8'): int
- {
- if (! isset($char[1])) {
- return ord($char);
- }
-
- if ($encoding !== 'UCS-4BE') {
- $char = mb_convert_encoding($char, 'UCS-4BE', $encoding);
- assert(is_string($char), 'format string is statically known to be correct');
- }
-
- $unpacked = unpack('N', $char);
- assert(is_array($unpacked), 'format string is statically known to be correct');
-
- return $unpacked[1];
- }
-
- /** Returns UTF-8 char code at given $positing of the $string. */
- public static function charCodeAt(string $string, int $position): int
- {
- $char = mb_substr($string, $position, 1, 'UTF-8');
-
- return self::ord($char);
- }
-
- /** @throws \JsonException */
- public static function printCharCode(?int $code): string
- {
- if ($code === null) {
- return '<EOF>';
- }
-
- return $code < 0x007F
- // Trust JSON for ASCII
- ? json_encode(self::chr($code), JSON_THROW_ON_ERROR)
- // Otherwise, print the escaped form
- : '"\\u' . dechex($code) . '"';
- }
-
- /**
- * Upholds the spec rules about naming.
- *
- * @throws Error
- */
- public static function assertValidName(string $name): void
- {
- $error = self::isValidNameError($name);
- if ($error !== null) {
- throw $error;
- }
- }
-
- /** Returns an Error if a name is invalid. */
- public static function isValidNameError(string $name, ?Node $node = null): ?Error
- {
- if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
- return new Error(
- "Name \"{$name}\" must not begin with \"__\", which is reserved by Automattic\WooCommerce\Vendor\GraphQL introspection.",
- $node
- );
- }
-
- if (preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name) !== 1) {
- return new Error(
- "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
- $node
- );
- }
-
- return null;
- }
-
- /** @param array<string> $items */
- public static function quotedOrList(array $items): string
- {
- $quoted = array_map(
- static fn (string $item): string => "\"{$item}\"",
- $items
- );
-
- return self::orList($quoted);
- }
-
- /** @param array<string> $items */
- public static function orList(array $items): string
- {
- if ($items === []) {
- return '';
- }
-
- $selected = array_slice($items, 0, 5);
- $selectedLength = count($selected);
- $firstSelected = $selected[0];
-
- if ($selectedLength === 1) {
- return $firstSelected;
- }
-
- return array_reduce(
- range(1, $selectedLength - 1),
- static fn ($list, $index): string => $list
- . ($selectedLength > 2 ? ', ' : ' ')
- . ($index === $selectedLength - 1 ? 'or ' : '')
- . $selected[$index],
- $firstSelected
- );
- }
-
- /**
- * Given an invalid input string and a list of valid options, returns a filtered
- * list of valid options sorted based on their similarity with the input.
- *
- * @param array<string> $options
- *
- * @return array<int, string>
- */
- public static function suggestionList(string $input, array $options): array
- {
- /** @var array<string, int> $optionsByDistance */
- $optionsByDistance = [];
- $lexicalDistance = new LexicalDistance($input);
- $threshold = mb_strlen($input) * 0.4 + 1;
- foreach ($options as $option) {
- $distance = $lexicalDistance->measure($option, $threshold);
-
- if ($distance !== null) {
- $optionsByDistance[$option] = $distance;
- }
- }
-
- uksort($optionsByDistance, static function (string $a, string $b) use ($optionsByDistance) {
- $distanceDiff = $optionsByDistance[$a] - $optionsByDistance[$b];
-
- return $distanceDiff !== 0 ? $distanceDiff : strnatcmp($a, $b);
- });
-
- return array_map('strval', array_keys($optionsByDistance));
- }
-
- /**
- * Try to extract the value for a key from an object like value.
- *
- * @param mixed $objectLikeValue
- *
- * @return mixed
- */
- public static function extractKey($objectLikeValue, string $key)
- {
- if (is_array($objectLikeValue) || $objectLikeValue instanceof \ArrayAccess) {
- return $objectLikeValue[$key] ?? null;
- }
-
- if (is_object($objectLikeValue)) {
- return $objectLikeValue->{$key} ?? null;
- }
-
- return null;
- }
-
- /**
- * Split a string that has either Unix, Windows or Mac style newlines into lines.
- *
- * @return list<string>
- */
- public static function splitLines(string $value): array
- {
- $lines = preg_split("/\r\n|\r|\n/", $value);
- assert(is_array($lines), 'given the regex is valid');
-
- return $lines;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php
deleted file mode 100644
index 06e36a77fa2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\CoercionError;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-/**
- * @phpstan-type CoercedValue array{errors: null, value: mixed}
- * @phpstan-type CoercedErrors array{errors: array<int, CoercionError>, value: null}
- *
- * @phpstan-import-type InputPath from CoercionError
- */
-class Value
-{
- /**
- * Coerce the given value to match the given Automattic\WooCommerce\Vendor\GraphQL Input Type.
- *
- * Returns either a value which is valid for the provided type,
- * or a list of encountered coercion errors.
- *
- * @param mixed $value
- * @param InputType&Type $type
- *
- * @phpstan-param InputPath|null $path
- *
- * @throws InvariantViolation
- *
- * @phpstan-return CoercedValue|CoercedErrors
- */
- public static function coerceInputValue($value, InputType $type, ?array $path = null, ?Schema $schema = null): array
- {
- if ($type instanceof NonNull) {
- if ($value === null) {
- return self::ofErrors([
- CoercionError::make("Expected non-nullable type \"{$type}\" not to be null.", $path, $value),
- ]);
- }
-
- // @phpstan-ignore-next-line wrapped type is known to be input type after schema validation
- return self::coerceInputValue($value, $type->getWrappedType(), $path, $schema);
- }
-
- if ($value === null) {
- // Explicitly return the value null.
- return self::ofValue(null);
- }
-
- // Account for type loader returning a different scalar instance than
- // the built-in singleton used in field definitions. Resolve the actual
- // type from the schema to ensure the correct parseValue() is called.
- if ($schema !== null && Type::isBuiltInScalar($type)) {
- $schemaType = $schema->getType($type->name);
- assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$type->name}\".");
- $type = $schemaType;
- }
-
- if ($type instanceof ScalarType || $type instanceof EnumType) {
- try {
- return self::ofValue($type->parseValue($value));
- } catch (\Throwable $error) {
- if (
- $error instanceof Error
- || ($error instanceof ClientAware && $error->isClientSafe())
- ) {
- return self::ofErrors([
- CoercionError::make($error->getMessage(), $path, $value, $error),
- ]);
- }
-
- return self::ofErrors([
- CoercionError::make("Expected type \"{$type->name}\".", $path, $value, $error),
- ]);
- }
- }
-
- if ($type instanceof ListOfType) {
- $itemType = $type->getWrappedType();
- assert($itemType instanceof InputType, 'known through schema validation');
-
- if (is_iterable($value)) {
- $errors = [];
- $coercedValue = [];
- foreach ($value as $index => $itemValue) {
- $coercedItem = self::coerceInputValue(
- $itemValue,
- $itemType,
- [...$path ?? [], $index],
- $schema,
- );
-
- if (isset($coercedItem['errors'])) {
- $errors = self::add($errors, $coercedItem['errors']);
- } else {
- $coercedValue[] = $coercedItem['value'];
- }
- }
-
- return $errors === []
- ? self::ofValue($coercedValue)
- : self::ofErrors($errors);
- }
-
- // Lists accept a non-list value as a list of one.
- $coercedItem = self::coerceInputValue($value, $itemType, null, $schema);
-
- return isset($coercedItem['errors'])
- ? $coercedItem
- : self::ofValue([$coercedItem['value']]);
- }
-
- assert($type instanceof InputObjectType, 'we handled all other cases at this point');
-
- if ($value instanceof \stdClass) {
- // Cast objects to associative array before checking the fields.
- // Note that the coerced value will be an array.
- $value = (array) $value;
- } elseif (! is_array($value)) {
- return self::ofErrors([
- CoercionError::make("Expected type \"{$type->name}\" to be an object.", $path, $value),
- ]);
- }
-
- $errors = [];
- $coercedValue = [];
- $fields = $type->getFields();
- foreach ($fields as $fieldName => $field) {
- if (array_key_exists($fieldName, $value)) {
- $fieldValue = $value[$fieldName];
- $coercedField = self::coerceInputValue(
- $fieldValue,
- $field->getType(),
- [...$path ?? [], $fieldName],
- $schema,
- );
-
- if (isset($coercedField['errors'])) {
- $errors = self::add($errors, $coercedField['errors']);
- } else {
- $coercedValue[$fieldName] = $coercedField['value'];
- }
- } elseif ($field->defaultValueExists()) {
- $coercedValue[$fieldName] = $field->defaultValue;
- } elseif ($field->getType() instanceof NonNull) {
- $errors = self::add(
- $errors,
- CoercionError::make("Field \"{$fieldName}\" of required type \"{$field->getType()->toString()}\" was not provided.", $path, $value)
- );
- }
- }
-
- // Ensure every provided field is defined.
- foreach ($value as $fieldName => $field) {
- if (array_key_exists($fieldName, $fields)) {
- continue;
- }
-
- $suggestions = Utils::suggestionList(
- (string) $fieldName,
- array_keys($fields)
- );
- $message = "Field \"{$fieldName}\" is not defined by type \"{$type->name}\"."
- . ($suggestions === []
- ? ''
- : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?');
-
- $errors = self::add(
- $errors,
- CoercionError::make($message, $path, $value)
- );
- }
-
- // Validate OneOf constraints if this is a OneOf input type
- if ($type->isOneOf()) {
- $providedFieldCount = count($coercedValue);
- $nullFieldName = null;
-
- if ($providedFieldCount !== 1) {
- $errors = self::add(
- $errors,
- CoercionError::make("OneOf input object \"{$type->name}\" must specify exactly one field.", $path, $value)
- );
- } else {
- foreach ($coercedValue as $fieldName => $fieldValue) {
- if ($fieldValue === null) {
- $nullFieldName = $fieldName;
- }
- }
-
- if ($nullFieldName !== null) {
- $errors = self::add(
- $errors,
- CoercionError::make("OneOf input object \"{$type->name}\" field \"{$nullFieldName}\" must be non-null.", $path, $value)
- );
- }
- }
- }
-
- return $errors === []
- ? self::ofValue($type->parseValue($coercedValue))
- : self::ofErrors($errors);
- }
-
- /**
- * @param array<int, CoercionError> $errors
- *
- * @phpstan-return CoercedErrors
- */
- private static function ofErrors(array $errors): array
- {
- return ['errors' => $errors, 'value' => null];
- }
-
- /**
- * @param mixed $value any value
- *
- * @phpstan-return CoercedValue
- */
- private static function ofValue($value): array
- {
- return ['errors' => null, 'value' => $value];
- }
-
- /**
- * @param array<int, CoercionError> $errors
- * @param CoercionError|array<int, CoercionError> $errorOrErrors
- *
- * @return array<int, CoercionError>
- */
- private static function add(array $errors, $errorOrErrors): array
- {
- $moreErrors = is_array($errorOrErrors)
- ? $errorOrErrors
- : [$errorOrErrors];
-
- return array_merge($errors, $moreErrors);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php
deleted file mode 100644
index 1dad7900f05..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php
+++ /dev/null
@@ -1,330 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\DisableIntrospection;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ExecutableDefinitions;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\FieldsOnCorrectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownArgumentNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownDirectives;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownFragmentNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownTypeNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\LoneAnonymousOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\LoneSchemaDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoFragmentCycles;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUndefinedVariables;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUnusedFragments;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUnusedVariables;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OneOfInputObjectsRule;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\PossibleFragmentSpreads;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\PossibleTypeExtensions;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ProvidedRequiredArguments;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryDepth;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QuerySecurityRule;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ScalarLeafs;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\SingleFieldSubscription;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueArgumentDefinitionNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueArgumentNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueDirectiveNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueEnumValueNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueFieldDefinitionNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueFragmentNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueInputFieldNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueOperationNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueOperationTypes;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueTypeNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueVariableNames;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValuesOfCorrectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\VariablesAreInputTypes;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\VariablesInAllowedPosition;
-
-/**
- * Implements the "Validation" section of the spec.
- *
- * Validation runs synchronously, returning an array of encountered errors, or
- * an empty array if no errors were encountered and the document is valid.
- *
- * A list of specific validation rules may be provided. If not provided, the
- * default list of rules defined by the Automattic\WooCommerce\Vendor\GraphQL specification will be used.
- *
- * Each validation rule is an instance of Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule
- * which returns a visitor (see the [Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor API](class-reference.md#graphqllanguagevisitor)).
- *
- * Visitor methods are expected to return an instance of [Automattic\WooCommerce\Vendor\GraphQL\Error\Error](class-reference.md#graphqlerrorerror),
- * or array of such instances when invalid.
- *
- * Optionally a custom TypeInfo instance may be provided. If not provided, one
- * will be created from the provided schema.
- */
-class DocumentValidator
-{
- /** @var array<string, ValidationRule> */
- private static array $rules = [];
-
- /** @var array<class-string<ValidationRule>, ValidationRule> */
- private static array $defaultRules;
-
- /** @var array<class-string<QuerySecurityRule>, QuerySecurityRule> */
- private static array $securityRules;
-
- /** @var array<class-string<ValidationRule>, ValidationRule> */
- private static array $sdlRules;
-
- private static bool $initRules = false;
-
- /**
- * Validate a Automattic\WooCommerce\Vendor\GraphQL query against a schema.
- *
- * @param array<ValidationRule>|null $rules Defaults to using all available rules
- *
- * @throws \Exception
- *
- * @return list<Error>
- *
- * @api
- */
- public static function validate(
- Schema $schema,
- DocumentNode $ast,
- ?array $rules = null,
- ?TypeInfo $typeInfo = null
- ): array {
- $rules ??= static::allRules();
-
- if ($rules === []) {
- return [];
- }
-
- $typeInfo ??= new TypeInfo($schema);
-
- $context = new QueryValidationContext($schema, $ast, $typeInfo);
-
- $visitors = [];
- foreach ($rules as $rule) {
- $visitors[] = $rule->getVisitor($context);
- }
-
- Visitor::visit(
- $ast,
- Visitor::visitWithTypeInfo(
- $typeInfo,
- Visitor::visitInParallel($visitors)
- )
- );
-
- return $context->getErrors();
- }
-
- /**
- * Returns all global validation rules.
- *
- * @throws \InvalidArgumentException
- *
- * @return array<string, ValidationRule>
- *
- * @api
- */
- public static function allRules(): array
- {
- if (! self::$initRules) {
- self::$rules = array_merge(
- static::defaultRules(),
- self::securityRules(),
- self::$rules
- );
- self::$initRules = true;
- }
-
- return self::$rules;
- }
-
- /** @return array<class-string<ValidationRule>, ValidationRule> */
- public static function defaultRules(): array
- {
- return self::$defaultRules ??= [
- ExecutableDefinitions::class => new ExecutableDefinitions(),
- UniqueOperationNames::class => new UniqueOperationNames(),
- LoneAnonymousOperation::class => new LoneAnonymousOperation(),
- SingleFieldSubscription::class => new SingleFieldSubscription(),
- KnownTypeNames::class => new KnownTypeNames(),
- FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
- VariablesAreInputTypes::class => new VariablesAreInputTypes(),
- ScalarLeafs::class => new ScalarLeafs(),
- FieldsOnCorrectType::class => new FieldsOnCorrectType(),
- UniqueFragmentNames::class => new UniqueFragmentNames(),
- KnownFragmentNames::class => new KnownFragmentNames(),
- NoUnusedFragments::class => new NoUnusedFragments(),
- PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
- NoFragmentCycles::class => new NoFragmentCycles(),
- UniqueVariableNames::class => new UniqueVariableNames(),
- NoUndefinedVariables::class => new NoUndefinedVariables(),
- NoUnusedVariables::class => new NoUnusedVariables(),
- KnownDirectives::class => new KnownDirectives(),
- UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
- KnownArgumentNames::class => new KnownArgumentNames(),
- UniqueArgumentNames::class => new UniqueArgumentNames(),
- ValuesOfCorrectType::class => new ValuesOfCorrectType(),
- ProvidedRequiredArguments::class => new ProvidedRequiredArguments(),
- VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
- OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
- UniqueInputFieldNames::class => new UniqueInputFieldNames(),
- OneOfInputObjectsRule::class => new OneOfInputObjectsRule(),
- ];
- }
-
- /**
- * @deprecated just add rules via @see DocumentValidator::addRule()
- *
- * @throws \InvalidArgumentException
- *
- * @return array<class-string<QuerySecurityRule>, QuerySecurityRule>
- */
- public static function securityRules(): array
- {
- return self::$securityRules ??= [
- DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED),
- QueryDepth::class => new QueryDepth(QueryDepth::DISABLED),
- QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED),
- ];
- }
-
- /** @return array<class-string<ValidationRule>, ValidationRule> */
- public static function sdlRules(): array
- {
- return self::$sdlRules ??= [
- LoneSchemaDefinition::class => new LoneSchemaDefinition(),
- UniqueOperationTypes::class => new UniqueOperationTypes(),
- UniqueTypeNames::class => new UniqueTypeNames(),
- UniqueEnumValueNames::class => new UniqueEnumValueNames(),
- UniqueFieldDefinitionNames::class => new UniqueFieldDefinitionNames(),
- UniqueArgumentDefinitionNames::class => new UniqueArgumentDefinitionNames(),
- UniqueDirectiveNames::class => new UniqueDirectiveNames(),
- KnownTypeNames::class => new KnownTypeNames(),
- KnownDirectives::class => new KnownDirectives(),
- UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
- PossibleTypeExtensions::class => new PossibleTypeExtensions(),
- KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
- UniqueArgumentNames::class => new UniqueArgumentNames(),
- UniqueInputFieldNames::class => new UniqueInputFieldNames(),
- ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
- ];
- }
-
- /**
- * Returns global validation rule by name.
- *
- * Standard rules are named by class name, so example usage for such rules:
- *
- * @example DocumentValidator::getRule(Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity::class);
- *
- * @api
- *
- * @throws \InvalidArgumentException
- */
- public static function getRule(string $name): ?ValidationRule
- {
- return static::allRules()[$name] ?? null;
- }
-
- /**
- * Add rule to list of global validation rules.
- *
- * @api
- */
- public static function addRule(ValidationRule $rule): void
- {
- self::$rules[$rule->getName()] = $rule;
- }
-
- /**
- * Remove rule from list of global validation rules.
- *
- * @api
- */
- public static function removeRule(ValidationRule $rule): void
- {
- unset(self::$rules[$rule->getName()]);
- }
-
- /**
- * Validate a Automattic\WooCommerce\Vendor\GraphQL document defined through schema definition language.
- *
- * @param array<ValidationRule>|null $rules
- *
- * @throws \Exception
- *
- * @return list<Error>
- */
- public static function validateSDL(
- DocumentNode $documentAST,
- ?Schema $schemaToExtend = null,
- ?array $rules = null
- ): array {
- $rules ??= self::sdlRules();
-
- if ($rules === []) {
- return [];
- }
-
- $context = new SDLValidationContext($documentAST, $schemaToExtend);
-
- $visitors = [];
- foreach ($rules as $rule) {
- $visitors[] = $rule->getSDLVisitor($context);
- }
-
- Visitor::visit(
- $documentAST,
- Visitor::visitInParallel($visitors)
- );
-
- return $context->getErrors();
- }
-
- /**
- * @throws \Exception
- * @throws Error
- */
- public static function assertValidSDL(DocumentNode $documentAST): void
- {
- $errors = self::validateSDL($documentAST);
- if ($errors !== []) {
- throw new Error(self::combineErrorMessages($errors));
- }
- }
-
- /**
- * @throws \Exception
- * @throws Error
- */
- public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema): void
- {
- $errors = self::validateSDL($documentAST, $schema);
- if ($errors !== []) {
- throw new Error(self::combineErrorMessages($errors));
- }
- }
-
- /** @param array<Error> $errors */
- private static function combineErrorMessages(array $errors): string
- {
- $messages = [];
- foreach ($errors as $error) {
- $messages[] = $error->getMessage();
- }
-
- return implode("\n\n", $messages);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php
deleted file mode 100644
index 74488a103e4..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php
+++ /dev/null
@@ -1,276 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\HasSelectionSet;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
-
-/**
- * An instance of this class is passed as the "this" context to all validators,
- * allowing access to commonly useful contextual information from within a
- * validation rule.
- *
- * @phpstan-type VariableUsage array{node: VariableNode, type: (Type&InputType)|null, defaultValue: mixed}
- */
-class QueryValidationContext implements ValidationContext
-{
- protected Schema $schema;
-
- protected DocumentNode $ast;
-
- /** @var list<Error> */
- protected array $errors = [];
-
- private TypeInfo $typeInfo;
-
- /** @var array<string, FragmentDefinitionNode> */
- private array $fragments;
-
- /** @var \SplObjectStorage<HasSelectionSet, array<int, FragmentSpreadNode>> */
- private \SplObjectStorage $fragmentSpreads;
-
- /** @var \SplObjectStorage<OperationDefinitionNode, array<int, FragmentDefinitionNode>> */
- private \SplObjectStorage $recursivelyReferencedFragments;
-
- /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */
- private \SplObjectStorage $variableUsages;
-
- /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */
- private \SplObjectStorage $recursiveVariableUsages;
-
- public function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
- {
- $this->schema = $schema;
- $this->ast = $ast;
- $this->typeInfo = $typeInfo;
-
- $this->fragmentSpreads = new \SplObjectStorage();
- $this->recursivelyReferencedFragments = new \SplObjectStorage();
- $this->variableUsages = new \SplObjectStorage();
- $this->recursiveVariableUsages = new \SplObjectStorage();
- }
-
- public function reportError(Error $error): void
- {
- $this->errors[] = $error;
- }
-
- /** @return list<Error> */
- public function getErrors(): array
- {
- return $this->errors;
- }
-
- public function getDocument(): DocumentNode
- {
- return $this->ast;
- }
-
- public function getSchema(): Schema
- {
- return $this->schema;
- }
-
- /**
- * @throws \Exception
- *
- * @phpstan-return array<int, VariableUsage>
- */
- public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array
- {
- $usages = $this->recursiveVariableUsages[$operation] ?? null;
-
- if ($usages === null) {
- $usages = $this->getVariableUsages($operation);
- $fragments = $this->getRecursivelyReferencedFragments($operation);
-
- $allUsages = [$usages];
- foreach ($fragments as $fragment) {
- $allUsages[] = $this->getVariableUsages($fragment);
- }
-
- $usages = array_merge(...$allUsages);
- $this->recursiveVariableUsages[$operation] = $usages;
- }
-
- return $usages;
- }
-
- /**
- * @param HasSelectionSet&Node $node
- *
- * @throws \Exception
- *
- * @phpstan-return array<int, VariableUsage>
- */
- private function getVariableUsages(HasSelectionSet $node): array
- {
- if (! isset($this->variableUsages[$node])) {
- $usages = [];
- $typeInfo = new TypeInfo($this->schema);
- Visitor::visit(
- $node,
- Visitor::visitWithTypeInfo(
- $typeInfo,
- [
- NodeKind::VARIABLE_DEFINITION => static fn () => Visitor::skipNode(),
- NodeKind::VARIABLE => static function (VariableNode $variable) use (&$usages, $typeInfo): void {
- $usages[] = [
- 'node' => $variable,
- 'type' => $typeInfo->getInputType(),
- 'defaultValue' => $typeInfo->getDefaultValue(),
- ];
- },
- ]
- )
- );
-
- return $this->variableUsages[$node] = $usages;
- }
-
- return $this->variableUsages[$node];
- }
-
- /** @return array<int, FragmentDefinitionNode> */
- public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array
- {
- $fragments = $this->recursivelyReferencedFragments[$operation] ?? null;
-
- if ($fragments === null) {
- $fragments = [];
- $collectedNames = [];
- $nodesToVisit = [$operation];
- while ($nodesToVisit !== []) {
- $node = array_pop($nodesToVisit);
- $spreads = $this->getFragmentSpreads($node);
- foreach ($spreads as $spread) {
- $fragName = $spread->name->value;
-
- if ($collectedNames[$fragName] ?? false) {
- continue;
- }
-
- $collectedNames[$fragName] = true;
- $fragment = $this->getFragment($fragName);
- if ($fragment === null) {
- continue;
- }
-
- $fragments[] = $fragment;
- $nodesToVisit[] = $fragment;
- }
- }
-
- $this->recursivelyReferencedFragments[$operation] = $fragments;
- }
-
- return $fragments;
- }
-
- /**
- * @param OperationDefinitionNode|FragmentDefinitionNode $node
- *
- * @return array<int, FragmentSpreadNode>
- */
- public function getFragmentSpreads(HasSelectionSet $node): array
- {
- $spreads = $this->fragmentSpreads[$node] ?? null;
- if ($spreads === null) {
- $spreads = [];
-
- $setsToVisit = [$node->getSelectionSet()];
- while ($setsToVisit !== []) {
- $set = array_pop($setsToVisit);
-
- foreach ($set->selections as $selection) {
- if ($selection instanceof FragmentSpreadNode) {
- $spreads[] = $selection;
- } else {
- assert($selection instanceof FieldNode || $selection instanceof InlineFragmentNode);
-
- $selectionSet = $selection->selectionSet;
- if ($selectionSet !== null) {
- $setsToVisit[] = $selectionSet;
- }
- }
- }
- }
-
- $this->fragmentSpreads[$node] = $spreads;
- }
-
- return $spreads;
- }
-
- public function getFragment(string $name): ?FragmentDefinitionNode
- {
- if (! isset($this->fragments)) {
- $fragments = [];
- foreach ($this->getDocument()->definitions as $statement) {
- if ($statement instanceof FragmentDefinitionNode) {
- $fragments[$statement->name->value] = $statement;
- }
- }
-
- $this->fragments = $fragments;
- }
-
- return $this->fragments[$name] ?? null;
- }
-
- public function getType(): ?Type
- {
- return $this->typeInfo->getType();
- }
-
- /** @return (CompositeType&Type)|null */
- public function getParentType(): ?CompositeType
- {
- return $this->typeInfo->getParentType();
- }
-
- /** @return (Type&InputType)|null */
- public function getInputType(): ?InputType
- {
- return $this->typeInfo->getInputType();
- }
-
- /** @return (Type&InputType)|null */
- public function getParentInputType(): ?InputType
- {
- return $this->typeInfo->getParentInputType();
- }
-
- public function getFieldDef(): ?FieldDefinition
- {
- return $this->typeInfo->getFieldDef();
- }
-
- public function getDirective(): ?Directive
- {
- return $this->typeInfo->getDirective();
- }
-
- public function getArgument(): ?Argument
- {
- return $this->typeInfo->getArgument();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php
deleted file mode 100644
index 32c90019f10..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * @see Node, VisitorOperation
- *
- * @phpstan-type NodeVisitorFnResult VisitorOperation|mixed|null
- * @phpstan-type VisitorFnResult array<string, callable(Node): NodeVisitorFnResult>|array<string, array<string, callable(Node): NodeVisitorFnResult>>
- * @phpstan-type VisitorFn callable(ValidationContext): VisitorFnResult
- */
-class CustomValidationRule extends ValidationRule
-{
- /**
- * @var callable
- *
- * @phpstan-var VisitorFn
- */
- protected $visitorFn;
-
- /** @phpstan-param VisitorFn $visitorFn */
- public function __construct(string $name, callable $visitorFn)
- {
- $this->name = $name;
- $this->visitorFn = $visitorFn;
- }
-
- public function getVisitor(ValidationContext $context): array
- {
- return ($this->visitorFn)($context);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php
deleted file mode 100644
index 378299ccd93..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class DisableIntrospection extends QuerySecurityRule
-{
- public const ENABLED = 1;
-
- protected int $isEnabled;
-
- public function __construct(int $enabled)
- {
- $this->setEnabled($enabled);
- }
-
- public function setEnabled(int $enabled): void
- {
- $this->isEnabled = $enabled;
- }
-
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->invokeIfNeeded(
- $context,
- [
- NodeKind::FIELD => static function (FieldNode $node) use ($context): void {
- if ($node->name->value !== '__type' && $node->name->value !== '__schema') {
- return;
- }
-
- $context->reportError(new Error(
- static::introspectionDisabledMessage(),
- [$node]
- ));
- },
- ]
- );
- }
-
- public static function introspectionDisabledMessage(): string
- {
- return 'Automattic\WooCommerce\Vendor\GraphQL introspection is not allowed, but the query contained __schema or __type';
- }
-
- protected function isEnabled(): bool
- {
- return $this->isEnabled !== self::DISABLED;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php
deleted file mode 100644
index 1270f7517d2..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ExecutableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * Executable definitions.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid for execution if all definitions are either
- * operation or fragment definitions.
- */
-class ExecutableDefinitions extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context): VisitorOperation {
- foreach ($node->definitions as $definition) {
- if (! $definition instanceof ExecutableDefinitionNode) {
- if ($definition instanceof SchemaDefinitionNode || $definition instanceof SchemaExtensionNode) {
- $defName = 'schema';
- } else {
- assert(
- $definition instanceof TypeDefinitionNode || $definition instanceof TypeExtensionNode,
- 'only other option'
- );
- $defName = "\"{$definition->getName()->value}\"";
- }
-
- $context->reportError(new Error(
- static::nonExecutableDefinitionMessage($defName),
- [$definition]
- ));
- }
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-
- public static function nonExecutableDefinitionMessage(string $defName): string
- {
- return "The {$defName} definition is not executable.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php
deleted file mode 100644
index 23a25401a4c..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class FieldsOnCorrectType extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::FIELD => function (FieldNode $node) use ($context): void {
- $fieldDef = $context->getFieldDef();
- if ($fieldDef !== null && $fieldDef->isVisible()) {
- return;
- }
-
- $type = $context->getParentType();
- if (! $type instanceof NamedType) {
- return;
- }
-
- // This isn't valid. Let's find suggestions, if any.
- $schema = $context->getSchema();
- $fieldName = $node->name->value;
- // First determine if there are any suggested types to condition on.
- $suggestedTypeNames = $this->getSuggestedTypeNames($schema, $type, $fieldName);
- // If there are no suggested types, then perhaps this was a typo?
- $suggestedFieldNames = $suggestedTypeNames === []
- ? $this->getSuggestedFieldNames($type, $fieldName)
- : [];
-
- // Report an error, including helpful suggestions.
- $context->reportError(new Error(
- static::undefinedFieldMessage(
- $node->name->value,
- $type->name,
- $suggestedTypeNames,
- $suggestedFieldNames
- ),
- [$node]
- ));
- },
- ];
- }
-
- /**
- * Go through all implementations of a type, as well as the interfaces
- * that it implements. If any of those types include the provided field,
- * suggest them, sorted by how often the type is referenced, starting
- * with interfaces.
- *
- * @throws InvariantViolation
- *
- * @return array<int, string>
- */
- protected function getSuggestedTypeNames(Schema $schema, Type $type, string $fieldName): array
- {
- if (Type::isAbstractType($type)) {
- $suggestedObjectTypes = [];
- $interfaceUsageCount = [];
-
- foreach ($schema->getPossibleTypes($type) as $possibleType) {
- if (! $possibleType->hasField($fieldName)) {
- continue;
- }
-
- // This object type defines this field.
- $suggestedObjectTypes[] = $possibleType->name;
- foreach ($possibleType->getInterfaces() as $possibleInterface) {
- if (! $possibleInterface->hasField($fieldName)) {
- continue;
- }
-
- // This interface type defines this field.
- $interfaceUsageCount[$possibleInterface->name] = isset($interfaceUsageCount[$possibleInterface->name])
- ? $interfaceUsageCount[$possibleInterface->name] + 1
- : 0;
- }
- }
-
- // Suggest interface types based on how common they are.
- arsort($interfaceUsageCount);
- $suggestedInterfaceTypes = array_keys($interfaceUsageCount);
-
- // Suggest both interface and object types.
- return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
- }
-
- // Otherwise, must be an Object type, which does not have suggested types.
- return [];
- }
-
- /**
- * For the field name provided, determine if there are any similar field names
- * that may be the result of a typo.
- *
- * @throws InvariantViolation
- *
- * @return array<int, string>
- */
- protected function getSuggestedFieldNames(Type $type, string $fieldName): array
- {
- if ($type instanceof HasFieldsType) {
- return Utils::suggestionList(
- $fieldName,
- $type->getFieldNames()
- );
- }
-
- // Otherwise, must be a Union type, which does not define fields.
- return [];
- }
-
- /**
- * @param array<string> $suggestedTypeNames
- * @param array<string> $suggestedFieldNames
- */
- public static function undefinedFieldMessage(
- string $fieldName,
- string $type,
- array $suggestedTypeNames,
- array $suggestedFieldNames
- ): string {
- $message = "Cannot query field \"{$fieldName}\" on type \"{$type}\".";
-
- if ($suggestedTypeNames !== []) {
- $suggestions = Utils::quotedOrList($suggestedTypeNames);
-
- $message .= " Did you mean to use an inline fragment on {$suggestions}?";
- } elseif ($suggestedFieldNames !== []) {
- $suggestions = Utils::quotedOrList($suggestedFieldNames);
-
- $message .= " Did you mean {$suggestions}?";
- }
-
- return $message;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php
deleted file mode 100644
index 6e539b41a82..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class FragmentsOnCompositeTypes extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context): void {
- if ($node->typeCondition === null) {
- return;
- }
-
- $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition);
- if ($type === null || Type::isCompositeType($type)) {
- return;
- }
-
- $context->reportError(new Error(
- static::inlineFragmentOnNonCompositeErrorMessage($type->toString()),
- [$node->typeCondition]
- ));
- },
- NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context): void {
- $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition);
-
- if ($type === null || Type::isCompositeType($type)) {
- return;
- }
-
- $context->reportError(new Error(
- static::fragmentOnNonCompositeErrorMessage(
- $node->name->value,
- Printer::doPrint($node->typeCondition)
- ),
- [$node->typeCondition]
- ));
- },
- ];
- }
-
- public static function inlineFragmentOnNonCompositeErrorMessage(string $type): string
- {
- return "Fragment cannot condition on non composite type \"{$type}\".";
- }
-
- public static function fragmentOnNonCompositeErrorMessage(string $fragName, string $type): string
- {
- return "Fragment \"{$fragName}\" cannot condition on non composite type \"{$type}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php
deleted file mode 100644
index 095258fd417..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * Known argument names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL field is only valid if all supplied arguments are defined by
- * that field.
- */
-class KnownArgumentNames extends ValidationRule
-{
- /** @throws InvariantViolation */
- public function getVisitor(QueryValidationContext $context): array
- {
- $knownArgumentNamesOnDirectives = new KnownArgumentNamesOnDirectives();
-
- return $knownArgumentNamesOnDirectives->getVisitor($context) + [
- NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context): void {
- $argDef = $context->getArgument();
- if ($argDef !== null) {
- return;
- }
-
- $fieldDef = $context->getFieldDef();
- if ($fieldDef === null) {
- return;
- }
-
- $parentType = $context->getParentType();
- if (! $parentType instanceof NamedType) {
- return;
- }
-
- $context->reportError(new Error(
- static::unknownArgMessage(
- $node->name->value,
- $fieldDef->name,
- $parentType->name,
- Utils::suggestionList(
- $node->name->value,
- array_map(
- static fn (Argument $arg): string => $arg->name,
- $fieldDef->args
- )
- )
- ),
- [$node]
- ));
- },
- ];
- }
-
- /** @param array<string> $suggestedArgs */
- public static function unknownArgMessage(string $argName, string $fieldName, string $typeName, array $suggestedArgs): string
- {
- $message = "Unknown argument \"{$argName}\" on field \"{$fieldName}\" of type \"{$typeName}\".";
-
- if ($suggestedArgs !== []) {
- $suggestions = Utils::quotedOrList($suggestedArgs);
- $message .= " Did you mean {$suggestions}?";
- }
-
- return $message;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php
deleted file mode 100644
index 00f800b980e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * Known argument names on directives.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL directive is only valid if all supplied arguments are defined by
- * that field.
- *
- * @phpstan-import-type VisitorArray from Visitor
- */
-class KnownArgumentNamesOnDirectives extends ValidationRule
-{
- /** @param array<string> $suggestedArgs */
- public static function unknownDirectiveArgMessage(string $argName, string $directiveName, array $suggestedArgs): string
- {
- $message = "Unknown argument \"{$argName}\" on directive \"@{$directiveName}\".";
-
- if (isset($suggestedArgs[0])) {
- $suggestions = Utils::quotedOrList($suggestedArgs);
- $message .= " Did you mean {$suggestions}?";
- }
-
- return $message;
- }
-
- /** @throws InvariantViolation */
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @throws InvariantViolation */
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @phpstan-return VisitorArray
- */
- public function getASTVisitor(ValidationContext $context): array
- {
- $directiveArgs = [];
- $schema = $context->getSchema();
- $definedDirectives = $schema !== null
- ? $schema->getDirectives()
- : Directive::getInternalDirectives();
-
- foreach ($definedDirectives as $directive) {
- $directiveArgs[$directive->name] = array_map(
- static fn (Argument $arg): string => $arg->name,
- $directive->args
- );
- }
-
- $astDefinitions = $context->getDocument()->definitions;
- foreach ($astDefinitions as $def) {
- if ($def instanceof DirectiveDefinitionNode) {
- $argNames = [];
- foreach ($def->arguments as $arg) {
- $argNames[] = $arg->name->value;
- }
-
- $directiveArgs[$def->name->value] = $argNames;
- }
- }
-
- return [
- NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context): VisitorOperation {
- $directiveName = $directiveNode->name->value;
-
- if (! isset($directiveArgs[$directiveName])) {
- return Visitor::skipNode();
- }
- $knownArgs = $directiveArgs[$directiveName];
-
- foreach ($directiveNode->arguments as $argNode) {
- $argName = $argNode->name->value;
- if (! in_array($argName, $knownArgs, true)) {
- $suggestions = Utils::suggestionList($argName, $knownArgs);
- $context->reportError(new Error(
- static::unknownDirectiveArgMessage($argName, $directiveName, $suggestions),
- [$argNode]
- ));
- }
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php
deleted file mode 100644
index ff4e7c18f18..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * @phpstan-import-type VisitorArray from Visitor
- */
-class KnownDirectives extends ValidationRule
-{
- /** @throws InvariantViolation */
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @throws InvariantViolation */
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @phpstan-return VisitorArray
- */
- public function getASTVisitor(ValidationContext $context): array
- {
- $locationsMap = [];
- $schema = $context->getSchema();
- $definedDirectives = $schema === null
- ? Directive::getInternalDirectives()
- : $schema->getDirectives();
-
- foreach ($definedDirectives as $directive) {
- $locationsMap[$directive->name] = $directive->locations;
- }
-
- $astDefinition = $context->getDocument()->definitions;
-
- foreach ($astDefinition as $def) {
- if ($def instanceof DirectiveDefinitionNode) {
- $locationNames = [];
- foreach ($def->locations as $location) {
- $locationNames[] = $location->value;
- }
-
- $locationsMap[$def->name->value] = $locationNames;
- }
- }
-
- return [
- NodeKind::DIRECTIVE => function (
- DirectiveNode $node,
- $key,
- $parent,
- $path,
- $ancestors
- ) use (
- $context,
- $locationsMap
- ): void {
- $name = $node->name->value;
- $locations = $locationsMap[$name] ?? null;
-
- if ($locations === null) {
- $context->reportError(new Error(
- static::unknownDirectiveMessage($name),
- [$node]
- ));
-
- return;
- }
-
- $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
-
- if ($candidateLocation === '' || in_array($candidateLocation, $locations, true)) {
- return;
- }
-
- $context->reportError(
- new Error(
- static::misplacedDirectiveMessage($name, $candidateLocation),
- [$node]
- )
- );
- },
- ];
- }
-
- public static function unknownDirectiveMessage(string $directiveName): string
- {
- return "Unknown directive \"@{$directiveName}\".";
- }
-
- /**
- * @param array<Node|NodeList<Node>> $ancestors
- *
- * @throws \Exception
- */
- protected function getDirectiveLocationForASTPath(array $ancestors): string
- {
- $appliedTo = $ancestors[count($ancestors) - 1];
-
- switch (true) {
- case $appliedTo instanceof OperationDefinitionNode:
- switch ($appliedTo->operation) {
- case 'query':
- return DirectiveLocation::QUERY;
- case 'mutation':
- return DirectiveLocation::MUTATION;
- case 'subscription':
- return DirectiveLocation::SUBSCRIPTION;
- }
- // no break, since all possible cases were handled
- case $appliedTo instanceof FieldNode:
- return DirectiveLocation::FIELD;
- case $appliedTo instanceof FragmentSpreadNode:
- return DirectiveLocation::FRAGMENT_SPREAD;
- case $appliedTo instanceof InlineFragmentNode:
- return DirectiveLocation::INLINE_FRAGMENT;
- case $appliedTo instanceof FragmentDefinitionNode:
- return DirectiveLocation::FRAGMENT_DEFINITION;
- case $appliedTo instanceof VariableDefinitionNode:
- return DirectiveLocation::VARIABLE_DEFINITION;
- case $appliedTo instanceof SchemaDefinitionNode:
- case $appliedTo instanceof SchemaExtensionNode:
- return DirectiveLocation::SCHEMA;
- case $appliedTo instanceof ScalarTypeDefinitionNode:
- case $appliedTo instanceof ScalarTypeExtensionNode:
- return DirectiveLocation::SCALAR;
- case $appliedTo instanceof ObjectTypeDefinitionNode:
- case $appliedTo instanceof ObjectTypeExtensionNode:
- return DirectiveLocation::OBJECT;
- case $appliedTo instanceof FieldDefinitionNode:
- return DirectiveLocation::FIELD_DEFINITION;
- case $appliedTo instanceof InterfaceTypeDefinitionNode:
- case $appliedTo instanceof InterfaceTypeExtensionNode:
- return DirectiveLocation::IFACE;
- case $appliedTo instanceof UnionTypeDefinitionNode:
- case $appliedTo instanceof UnionTypeExtensionNode:
- return DirectiveLocation::UNION;
- case $appliedTo instanceof EnumTypeDefinitionNode:
- case $appliedTo instanceof EnumTypeExtensionNode:
- return DirectiveLocation::ENUM;
- case $appliedTo instanceof EnumValueDefinitionNode:
- return DirectiveLocation::ENUM_VALUE;
- case $appliedTo instanceof InputObjectTypeDefinitionNode:
- case $appliedTo instanceof InputObjectTypeExtensionNode:
- return DirectiveLocation::INPUT_OBJECT;
- case $appliedTo instanceof InputValueDefinitionNode:
- $parentNode = $ancestors[count($ancestors) - 3];
-
- return $parentNode instanceof InputObjectTypeDefinitionNode
- ? DirectiveLocation::INPUT_FIELD_DEFINITION
- : DirectiveLocation::ARGUMENT_DEFINITION;
- default:
- $unknownLocation = get_class($appliedTo);
- throw new \Exception("Unknown directive location: {$unknownLocation}.");
- }
- }
-
- public static function misplacedDirectiveMessage(string $directiveName, string $location): string
- {
- return "Directive \"{$directiveName}\" may not be used on \"{$location}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php
deleted file mode 100644
index 86be2006118..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class KnownFragmentNames extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context): void {
- $fragmentName = $node->name->value;
- $fragment = $context->getFragment($fragmentName);
- if ($fragment !== null) {
- return;
- }
-
- $context->reportError(new Error(
- static::unknownFragmentMessage($fragmentName),
- [$node->name]
- ));
- },
- ];
- }
-
- public static function unknownFragmentMessage(string $fragName): string
- {
- return "Unknown fragment \"{$fragName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php
deleted file mode 100644
index 20e9f72b57a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * Known type names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if referenced types (specifically
- * variable definitions and fragment conditions) are defined by the type schema.
- *
- * @phpstan-import-type VisitorArray from \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor
- */
-class KnownTypeNames extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @phpstan-return VisitorArray */
- public function getASTVisitor(ValidationContext $context): array
- {
- /** @var array<int, string> $definedTypes */
- $definedTypes = [];
- foreach ($context->getDocument()->definitions as $def) {
- if ($def instanceof TypeDefinitionNode) {
- $definedTypes[] = $def->getName()->value;
- }
- }
-
- return [
- NodeKind::NAMED_TYPE => static function (NamedTypeNode $node, $_1, $parent, $_2, $ancestors) use ($context, $definedTypes): void {
- $typeName = $node->name->value;
- $schema = $context->getSchema();
-
- if (in_array($typeName, $definedTypes, true)) {
- return;
- }
-
- if ($schema !== null && $schema->hasType($typeName)) {
- return;
- }
-
- $definitionNode = $ancestors[2] ?? $parent;
- $isSDL = $definitionNode instanceof TypeSystemDefinitionNode || $definitionNode instanceof TypeSystemExtensionNode;
- if ($isSDL && in_array($typeName, Type::BUILT_IN_TYPE_NAMES, true)) {
- return;
- }
-
- $existingTypesMap = $schema !== null
- ? $schema->getTypeMap()
- : [];
- $typeNames = [
- ...array_keys($existingTypesMap),
- ...$definedTypes,
- ];
- $context->reportError(new Error(
- static::unknownTypeMessage(
- $typeName,
- Utils::suggestionList(
- $typeName,
- $isSDL
- ? [...Type::BUILT_IN_TYPE_NAMES, ...$typeNames]
- : $typeNames
- )
- ),
- [$node]
- ));
- },
- ];
- }
-
- /** @param array<string> $suggestedTypes */
- public static function unknownTypeMessage(string $type, array $suggestedTypes): string
- {
- $message = "Unknown type \"{$type}\".";
-
- if ($suggestedTypes !== []) {
- $suggestionList = Utils::quotedOrList($suggestedTypes);
- $message .= " Did you mean {$suggestionList}?";
- }
-
- return $message;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php
deleted file mode 100644
index 660cb90edf9..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * Lone anonymous operation.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if when it contains an anonymous operation
- * (the query shorthand) that it contains only that one operation definition.
- */
-class LoneAnonymousOperation extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- $operationCount = 0;
-
- return [
- NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount): void {
- $operationCount = 0;
- foreach ($node->definitions as $definition) {
- if ($definition instanceof OperationDefinitionNode) {
- ++$operationCount;
- }
- }
- },
- NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (&$operationCount, $context): void {
- if ($node->name !== null || $operationCount <= 1) {
- return;
- }
-
- $context->reportError(
- new Error(static::anonOperationNotAloneMessage(), [$node])
- );
- },
- ];
- }
-
- public static function anonOperationNotAloneMessage(): string
- {
- return 'This anonymous operation must be the only defined operation.';
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php
deleted file mode 100644
index 284fa7a3679..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Lone schema definition.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if it contains only one schema definition.
- */
-class LoneSchemaDefinition extends ValidationRule
-{
- public static function schemaDefinitionNotAloneMessage(): string
- {
- return 'Must provide only one schema definition.';
- }
-
- public static function canNotDefineSchemaWithinExtensionMessage(): string
- {
- return 'Cannot define a new schema within a schema extension.';
- }
-
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $oldSchema = $context->getSchema();
- $alreadyDefined = $oldSchema === null
- ? false
- : (
- $oldSchema->astNode !== null
- || $oldSchema->getQueryType() !== null
- || $oldSchema->getMutationType() !== null
- || $oldSchema->getSubscriptionType() !== null
- );
-
- $schemaDefinitionsCount = 0;
-
- return [
- NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount): void {
- if ($alreadyDefined) {
- $context->reportError(new Error(static::canNotDefineSchemaWithinExtensionMessage(), $node));
-
- return;
- }
-
- if ($schemaDefinitionsCount > 0) {
- $context->reportError(new Error(static::schemaDefinitionNotAloneMessage(), $node));
- }
-
- ++$schemaDefinitionsCount;
- },
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php
deleted file mode 100644
index c3792272ef7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class NoFragmentCycles extends ValidationRule
-{
- /** @var array<string, bool> */
- protected array $visitedFrags;
-
- /** @var array<int, FragmentSpreadNode> */
- protected array $spreadPath;
-
- /** @var array<string, int|null> */
- protected array $spreadPathIndexByName;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- // Tracks already visited fragments to maintain O(N) and to ensure that cycles
- // are not redundantly reported.
- $this->visitedFrags = [];
-
- // Array of AST nodes used to produce meaningful errors
- $this->spreadPath = [];
-
- // Position in the spread path
- $this->spreadPathIndexByName = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
- NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation {
- $this->detectCycleRecursive($node, $context);
-
- return Visitor::skipNode();
- },
- ];
- }
-
- protected function detectCycleRecursive(FragmentDefinitionNode $fragment, QueryValidationContext $context): void
- {
- if (isset($this->visitedFrags[$fragment->name->value])) {
- return;
- }
-
- $fragmentName = $fragment->name->value;
- $this->visitedFrags[$fragmentName] = true;
-
- $spreadNodes = $context->getFragmentSpreads($fragment);
-
- if ($spreadNodes === []) {
- return;
- }
-
- $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
-
- foreach ($spreadNodes as $spreadNode) {
- $spreadName = $spreadNode->name->value;
- $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
-
- $this->spreadPath[] = $spreadNode;
- if ($cycleIndex === null) {
- $spreadFragment = $context->getFragment($spreadName);
- if ($spreadFragment !== null) {
- $this->detectCycleRecursive($spreadFragment, $context);
- }
- } else {
- $cyclePath = array_slice($this->spreadPath, $cycleIndex);
- $fragmentNames = [];
- foreach (array_slice($cyclePath, 0, -1) as $frag) {
- $fragmentNames[] = $frag->name->value;
- }
-
- $context->reportError(new Error(
- static::cycleErrorMessage($spreadName, $fragmentNames),
- $cyclePath
- ));
- }
-
- array_pop($this->spreadPath);
- }
-
- $this->spreadPathIndexByName[$fragmentName] = null;
- }
-
- /** @param array<string> $spreadNames */
- public static function cycleErrorMessage(string $fragName, array $spreadNames = []): string
- {
- $via = $spreadNames === []
- ? ''
- : ' via ' . implode(', ', $spreadNames);
-
- return "Cannot spread fragment \"{$fragName}\" within itself{$via}.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php
deleted file mode 100644
index 1d5878a403e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * A Automattic\WooCommerce\Vendor\GraphQL operation is only valid if all variables encountered, both directly
- * and via fragment spreads, are defined by that operation.
- */
-class NoUndefinedVariables extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- /** @var array<string, true> $variableNameDefined */
- $variableNameDefined = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => [
- 'enter' => static function () use (&$variableNameDefined): void {
- $variableNameDefined = [];
- },
- 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context): void {
- $usages = $context->getRecursiveVariableUsages($operation);
-
- foreach ($usages as $usage) {
- $node = $usage['node'];
- $varName = $node->name->value;
-
- if (! isset($variableNameDefined[$varName])) {
- $context->reportError(new Error(
- static::undefinedVarMessage(
- $varName,
- $operation->name !== null
- ? $operation->name->value
- : null
- ),
- [$node, $operation]
- ));
- }
- }
- },
- ],
- NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined): void {
- $variableNameDefined[$def->variable->name->value] = true;
- },
- ];
- }
-
- public static function undefinedVarMessage(string $varName, ?string $opName): string
- {
- return $opName === null
- ? "Variable \"\${$varName}\" is not defined by operation \"{$opName}\"."
- : "Variable \"\${$varName}\" is not defined.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php
deleted file mode 100644
index 6cd72e7d4ce..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class NoUnusedFragments extends ValidationRule
-{
- /** @var array<int, OperationDefinitionNode> */
- protected array $operationDefs;
-
- /** @var array<int, FragmentDefinitionNode> */
- protected array $fragmentDefs;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->operationDefs = [];
- $this->fragmentDefs = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => function ($node): VisitorOperation {
- $this->operationDefs[] = $node;
-
- return Visitor::skipNode();
- },
- NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def): VisitorOperation {
- $this->fragmentDefs[] = $def;
-
- return Visitor::skipNode();
- },
- NodeKind::DOCUMENT => [
- 'leave' => function () use ($context): void {
- $fragmentNameUsed = [];
-
- foreach ($this->operationDefs as $operation) {
- foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) {
- $fragmentNameUsed[$fragment->name->value] = true;
- }
- }
-
- foreach ($this->fragmentDefs as $fragmentDef) {
- $fragName = $fragmentDef->name->value;
-
- if (! isset($fragmentNameUsed[$fragName])) {
- $context->reportError(new Error(
- static::unusedFragMessage($fragName),
- [$fragmentDef]
- ));
- }
- }
- },
- ],
- ];
- }
-
- public static function unusedFragMessage(string $fragName): string
- {
- return "Fragment \"{$fragName}\" is never used.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php
deleted file mode 100644
index 2c3f95e33f5..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class NoUnusedVariables extends ValidationRule
-{
- /** @var array<int, VariableDefinitionNode> */
- protected array $variableDefs;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->variableDefs = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => [
- 'enter' => function (): void {
- $this->variableDefs = [];
- },
- 'leave' => function (OperationDefinitionNode $operation) use ($context): void {
- $variableNameUsed = [];
- $usages = $context->getRecursiveVariableUsages($operation);
- $opName = $operation->name !== null
- ? $operation->name->value
- : null;
-
- foreach ($usages as $usage) {
- $node = $usage['node'];
- $variableNameUsed[$node->name->value] = true;
- }
-
- foreach ($this->variableDefs as $variableDef) {
- $variableName = $variableDef->variable->name->value;
-
- if (! isset($variableNameUsed[$variableName])) {
- $context->reportError(new Error(
- static::unusedVariableMessage($variableName, $opName),
- [$variableDef]
- ));
- }
- }
- },
- ],
- NodeKind::VARIABLE_DEFINITION => function ($def): void {
- $this->variableDefs[] = $def;
- },
- ];
- }
-
- public static function unusedVariableMessage(string $varName, ?string $opName = null): string
- {
- return $opName !== null
- ? "Variable \"\${$varName}\" is never used in operation \"{$opName}\"."
- : "Variable \"\${$varName}\" is never used.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php
deleted file mode 100644
index 37e95e84df8..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * OneOf Input Objects validation rule.
- *
- * Validates that OneOf Input Objects have exactly one non-null field provided.
- */
-class OneOfInputObjectsRule extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::OBJECT => static function (ObjectValueNode $node) use ($context): void {
- $type = $context->getInputType();
-
- if ($type === null) {
- return;
- }
-
- $namedType = Type::getNamedType($type);
- if (! ($namedType instanceof InputObjectType)
- || ! $namedType->isOneOf()
- ) {
- return;
- }
-
- $providedFields = [];
- $nullFields = [];
-
- foreach ($node->fields as $fieldNode) {
- $fieldName = $fieldNode->name->value;
- $providedFields[] = $fieldName;
-
- // Check if the field value is explicitly null
- if ($fieldNode->value->kind === NodeKind::NULL) {
- $nullFields[] = $fieldName;
- }
- }
-
- $fieldCount = count($providedFields);
-
- if ($fieldCount === 0) {
- $context->reportError(new Error(
- static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name),
- [$node]
- ));
-
- return;
- }
-
- if ($fieldCount > 1) {
- $context->reportError(new Error(
- static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name, $fieldCount),
- [$node]
- ));
-
- return;
- }
-
- // At this point, $fieldCount === 1
- if (count($nullFields) > 0) {
- // Exactly one field provided, but it's null
- $context->reportError(new Error(
- static::oneOfInputObjectFieldValueMustNotBeNullMessage($namedType->name, $nullFields[0]),
- [$node]
- ));
- }
- },
- ];
- }
-
- public static function oneOfInputObjectExpectedExactlyOneFieldMessage(string $typeName, ?int $providedCount = null): string
- {
- if ($providedCount === null) {
- return "OneOf input object '{$typeName}' must specify exactly one field.";
- }
-
- return "OneOf input object '{$typeName}' must specify exactly one field, but {$providedCount} fields were provided.";
- }
-
- public static function oneOfInputObjectFieldValueMustNotBeNullMessage(string $typeName, string $fieldName): string
- {
- return "OneOf input object '{$typeName}' field '{$fieldName}' must be non-null.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php
deleted file mode 100644
index 163cadc23a1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php
+++ /dev/null
@@ -1,982 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\PairSet;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * ReasonOrReasons is recursive, but PHPStan does not support that.
- *
- * @phpstan-type ReasonOrReasons string|array<array{string, string|array<mixed>}>
- * @phpstan-type Conflict array{array{string, ReasonOrReasons}, array<int, FieldNode>, array<int, FieldNode>}
- * @phpstan-type FieldInfo array{Type|null, FieldNode, FieldDefinition|null}
- * @phpstan-type FieldMap array<string, array<int, FieldInfo>>
- */
-class OverlappingFieldsCanBeMerged extends ValidationRule
-{
- public const DEFAULT_MAX_COMPARISON_COUNT = 100_000;
-
- /**
- * A memoization for when two fragments are compared "between" each other for
- * conflicts. Two fragments may be compared many times, so memoizing this can
- * dramatically improve the performance of this validator.
- */
- protected PairSet $comparedFragmentPairs;
-
- /**
- * A cache for the "field map" and list of fragment names found in any given
- * selection set. Selection sets may be asked for this information multiple
- * times, so this improves the performance of this validator.
- *
- * @phpstan-var \SplObjectStorage<SelectionSetNode, array{FieldMap, array<int, string>}>
- */
- protected \SplObjectStorage $cachedFieldsAndFragmentNames;
-
- protected int $comparisonCount;
-
- protected int $comparisonLimit;
-
- public function __construct(int $comparisonLimit = self::DEFAULT_MAX_COMPARISON_COUNT)
- {
- $this->comparisonLimit = $comparisonLimit;
- }
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->comparedFragmentPairs = new PairSet();
- $this->cachedFieldsAndFragmentNames = new \SplObjectStorage();
- $this->comparisonCount = 0;
-
- return [
- NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void {
- $conflicts = $this->findConflictsWithinSelectionSet(
- $context,
- $context->getParentType(),
- $selectionSet
- );
-
- foreach ($conflicts as $conflict) {
- [[$responseName, $reason], $fields1, $fields2] = $conflict;
-
- $context->reportError(new Error(
- static::fieldsConflictMessage($responseName, $reason),
- array_merge($fields1, $fields2)
- ));
- }
- },
- ];
- }
-
- /**
- * Find all conflicts found "within" a selection set, including those found
- * via spreading in fragments. Called when visiting each SelectionSet in the
- * Automattic\WooCommerce\Vendor\GraphQL Document.
- *
- * @throws \Exception
- *
- * @phpstan-return array<int, Conflict>
- */
- protected function findConflictsWithinSelectionSet(
- QueryValidationContext $context,
- ?Type $parentType,
- SelectionSetNode $selectionSet
- ): array {
- [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames(
- $context,
- $parentType,
- $selectionSet
- );
-
- $conflicts = [];
-
- // (A) Find all conflicts "within" the fields of this selection set.
- // Note: this is the *only place* `collectConflictsWithin` is called.
- $this->collectConflictsWithin(
- $context,
- $conflicts,
- $fieldMap
- );
-
- $fragmentNamesLength = count($fragmentNames);
- if ($fragmentNamesLength !== 0) {
- // (B) Then collect conflicts between these fields and those represented by
- // each spread fragment name found.
- $comparedFragments = [];
- for ($i = 0; $i < $fragmentNamesLength; ++$i) {
- $this->collectConflictsBetweenFieldsAndFragment(
- $context,
- $conflicts,
- $comparedFragments,
- false,
- $fieldMap,
- $fragmentNames[$i]
- );
- // (C) Then compare this fragment with all other fragments found in this
- // selection set to collect conflicts between fragments spread together.
- // This compares each item in the list of fragment names to every other item
- // in that same list (except for itself).
- for ($j = $i + 1; $j < $fragmentNamesLength; ++$j) {
- $this->collectConflictsBetweenFragments(
- $context,
- $conflicts,
- false,
- $fragmentNames[$i],
- $fragmentNames[$j]
- );
- }
- }
- }
-
- return $conflicts;
- }
-
- /**
- * Given a selection set, return the collection of fields (a mapping of response
- * name to field ASTs and definitions) as well as a list of fragment names
- * referenced via fragment spreads.
- *
- * @throws \Exception
- *
- * @return array{FieldMap, array<int, string>}
- */
- protected function getFieldsAndFragmentNames(
- QueryValidationContext $context,
- ?Type $parentType,
- SelectionSetNode $selectionSet
- ): array {
- if (! isset($this->cachedFieldsAndFragmentNames[$selectionSet])) {
- /** @phpstan-var FieldMap $astAndDefs */
- $astAndDefs = [];
-
- /** @var array<string, bool> $fragmentNames */
- $fragmentNames = [];
-
- $this->internalCollectFieldsAndFragmentNames(
- $context,
- $parentType,
- $selectionSet,
- $astAndDefs,
- $fragmentNames
- );
-
- return $this->cachedFieldsAndFragmentNames[$selectionSet] = [$astAndDefs, array_keys($fragmentNames)];
- }
-
- return $this->cachedFieldsAndFragmentNames[$selectionSet];
- }
-
- /**
- * Algorithm:.
- *
- * Conflicts occur when two fields exist in a query which will produce the same
- * response name, but represent differing values, thus creating a conflict.
- * The algorithm below finds all conflicts via making a series of comparisons
- * between fields. In order to compare as few fields as possible, this makes
- * a series of comparisons "within" sets of fields and "between" sets of fields.
- *
- * Given any selection set, a collection produces both a set of fields by
- * also including all inline fragments, as well as a list of fragments
- * referenced by fragment spreads.
- *
- * A) Each selection set represented in the document first compares "within" its
- * collected set of fields, finding any conflicts between every pair of
- * overlapping fields.
- * Note: This is the *only time* that a the fields "within" a set are compared
- * to each other. After this only fields "between" sets are compared.
- *
- * B) Also, if any fragment is referenced in a selection set, then a
- * comparison is made "between" the original set of fields and the
- * referenced fragment.
- *
- * C) Also, if multiple fragments are referenced, then comparisons
- * are made "between" each referenced fragment.
- *
- * D) When comparing "between" a set of fields and a referenced fragment, first
- * a comparison is made between each field in the original set of fields and
- * each field in the the referenced set of fields.
- *
- * E) Also, if any fragment is referenced in the referenced selection set,
- * then a comparison is made "between" the original set of fields and the
- * referenced fragment (recursively referring to step D).
- *
- * F) When comparing "between" two fragments, first a comparison is made between
- * each field in the first referenced set of fields and each field in the the
- * second referenced set of fields.
- *
- * G) Also, any fragments referenced by the first must be compared to the
- * second, and any fragments referenced by the second must be compared to the
- * first (recursively referring to step F).
- *
- * H) When comparing two fields, if both have selection sets, then a comparison
- * is made "between" both selection sets, first comparing the set of fields in
- * the first selection set with the set of fields in the second.
- *
- * I) Also, if any fragment is referenced in either selection set, then a
- * comparison is made "between" the other set of fields and the
- * referenced fragment.
- *
- * J) Also, if two fragments are referenced in both selection sets, then a
- * comparison is made "between" the two fragments.
- */
-
- /**
- * Given a reference to a fragment, return the represented collection of fields
- * as well as a list of nested fragment names referenced via fragment spreads.
- *
- * @param array<string, bool> $fragmentNames
- *
- * @phpstan-param FieldMap $astAndDefs
- *
- * @throws \Exception
- */
- protected function internalCollectFieldsAndFragmentNames(
- QueryValidationContext $context,
- ?Type $parentType,
- SelectionSetNode $selectionSet,
- array &$astAndDefs,
- array &$fragmentNames
- ): void {
- foreach ($selectionSet->selections as $selection) {
- switch (true) {
- case $selection instanceof FieldNode:
- $fieldName = $selection->name->value;
- $fieldDef = null;
- if (
- ($parentType instanceof ObjectType || $parentType instanceof InterfaceType)
- && $parentType->hasField($fieldName)
- ) {
- $fieldDef = $parentType->getField($fieldName);
- }
-
- $responseName = $selection->alias->value ?? $fieldName;
-
- $astAndDefs[$responseName] ??= [];
- $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
- break;
- case $selection instanceof FragmentSpreadNode:
- $fragmentNames[$selection->name->value] = true;
- break;
- case $selection instanceof InlineFragmentNode:
- $typeCondition = $selection->typeCondition;
- $inlineFragmentType = $typeCondition === null
- ? $parentType
- : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);
-
- $this->internalCollectFieldsAndFragmentNames(
- $context,
- $inlineFragmentType,
- $selection->selectionSet,
- $astAndDefs,
- $fragmentNames
- );
- break;
- }
- }
- }
-
- /**
- * Collect all Conflicts "within" one collection of fields.
- *
- * @param array<int, Conflict> $conflicts
- *
- * @phpstan-param FieldMap $fieldMap
- *
- * @throws \Exception
- */
- protected function collectConflictsWithin(
- QueryValidationContext $context,
- array &$conflicts,
- array $fieldMap
- ): void {
- // A field map is a keyed collection, where each key represents a response
- // name and the value at that key is a list of all fields which provide that
- // response name. For every response name, if there are multiple fields, they
- // must be compared to find a potential conflict.
- foreach ($fieldMap as $responseName => $fields) {
- // This compares every field in the list to every other field in this list
- // (except to itself). If the list only has one item, nothing needs to
- // be compared.
- $fieldsLength = count($fields);
- if ($fieldsLength <= 1) {
- continue;
- }
-
- // Deduplicate structurally identical fields to avoid O(n²) blowup
- // when a query repeats the same field many times.
- $fields = $this->deduplicateFields($fields);
- $fieldsLength = count($fields);
- if ($fieldsLength <= 1) {
- continue;
- }
-
- for ($i = 0; $i < $fieldsLength; ++$i) {
- for ($j = $i + 1; $j < $fieldsLength; ++$j) {
- $conflict = $this->findConflict(
- $context,
- false, // within one collection is never mutually exclusive
- $responseName,
- $fields[$i],
- $fields[$j]
- );
- if ($conflict !== null) {
- $conflicts[] = $conflict;
- }
- }
- }
- }
- }
-
- /**
- * @phpstan-param array<int, FieldInfo> $fields
- *
- * @throws \JsonException
- *
- * @phpstan-return array<int, FieldInfo>
- */
- protected function deduplicateFields(array $fields): array
- {
- $unique = [];
- $seen = [];
- foreach ($fields as $field) {
- $key = $this->fieldFingerprint($field);
- if (! isset($seen[$key])) {
- $seen[$key] = true;
- $unique[] = $field;
- }
- }
-
- return $unique;
- }
-
- /**
- * @phpstan-param FieldInfo $field
- *
- * @throws \JsonException
- */
- protected function fieldFingerprint(array $field): string
- {
- [$parentType, $ast] = $field;
-
- $parentTypeId = $parentType !== null
- ? spl_object_id($parentType)
- : '';
- $name = $ast->name->value;
- $selectionSetId = $ast->selectionSet !== null
- ? spl_object_id($ast->selectionSet)
- : '';
-
- $fingerprint = "{$parentTypeId}:{$name}:{$selectionSetId}";
-
- foreach ($ast->arguments as $argument) {
- $fingerprint .= ":{$argument->name->value}=" . Printer::doPrint($argument->value);
- }
-
- return $fingerprint;
- }
-
- /**
- * Determines if there is a conflict between two particular fields, including
- * comparing their sub-fields.
- *
- * @param array{Type|null, FieldNode, FieldDefinition|null} $field1
- * @param array{Type|null, FieldNode, FieldDefinition|null} $field2
- *
- * @throws \Exception
- *
- * @phpstan-return Conflict|null
- */
- protected function findConflict(
- QueryValidationContext $context,
- bool $parentFieldsAreMutuallyExclusive,
- string $responseName,
- array $field1,
- array $field2
- ): ?array {
- if (++$this->comparisonCount > $this->comparisonLimit) {
- return [
- [$responseName, 'Too many field comparisons, query is too complex to validate'],
- [$field1[1]],
- [$field2[1]],
- ];
- }
-
- [$parentType1, $ast1, $def1] = $field1;
- [$parentType2, $ast2, $def2] = $field2;
-
- // If it is known that two fields could not possibly apply at the same
- // time, due to the parent types, then it is safe to permit them to diverge
- // in aliased field or arguments used as they will not present any ambiguity
- // by differing.
- // It is known that two parent types could never overlap if they are
- // different Object types. Interface or Union types might overlap - if not
- // in the current state of the schema, then perhaps in some future version,
- // thus may not safely diverge.
- $areMutuallyExclusive = $parentFieldsAreMutuallyExclusive
- || (
- $parentType1 !== $parentType2
- && $parentType1 instanceof ObjectType
- && $parentType2 instanceof ObjectType
- );
-
- // The return type for each field.
- $type1 = $def1 === null
- ? null
- : $def1->getType();
- $type2 = $def2 === null
- ? null
- : $def2->getType();
-
- if (! $areMutuallyExclusive) {
- // Two aliases must refer to the same field.
- $name1 = $ast1->name->value;
- $name2 = $ast2->name->value;
- if ($name1 !== $name2) {
- return [
- [$responseName, "{$name1} and {$name2} are different fields"],
- [$ast1],
- [$ast2],
- ];
- }
-
- if (! $this->sameArguments($ast1->arguments, $ast2->arguments)) {
- return [
- [$responseName, 'they have differing arguments'],
- [$ast1],
- [$ast2],
- ];
- }
- }
-
- if (
- $type1 !== null
- && $type2 !== null
- && $this->doTypesConflict($type1, $type2)
- ) {
- return [
- [$responseName, "they return conflicting types {$type1} and {$type2}"],
- [$ast1],
- [$ast2],
- ];
- }
-
- // Collect and compare sub-fields. Use the same "visited fragment names" list
- // for both collections so fields in a fragment reference are never
- // compared to themselves.
- $selectionSet1 = $ast1->selectionSet;
- $selectionSet2 = $ast2->selectionSet;
- if ($selectionSet1 !== null && $selectionSet2 !== null) {
- $conflicts = $this->findConflictsBetweenSubSelectionSets(
- $context,
- $areMutuallyExclusive,
- Type::getNamedType($type1),
- $selectionSet1,
- Type::getNamedType($type2),
- $selectionSet2
- );
-
- return $this->subfieldConflicts(
- $conflicts,
- $responseName,
- $ast1,
- $ast2
- );
- }
-
- return null;
- }
-
- /**
- * @param NodeList<ArgumentNode> $arguments1 keep
- * @param NodeList<ArgumentNode> $arguments2 keep
- *
- * @throws \JsonException
- */
- protected function sameArguments(NodeList $arguments1, NodeList $arguments2): bool
- {
- if (count($arguments1) !== count($arguments2)) {
- return false;
- }
-
- foreach ($arguments1 as $argument1) {
- $argument2 = null;
- foreach ($arguments2 as $argument) {
- if ($argument->name->value === $argument1->name->value) {
- $argument2 = $argument;
- break;
- }
- }
-
- if ($argument2 === null) {
- return false;
- }
-
- if (! $this->sameValue($argument1->value, $argument2->value)) {
- return false;
- }
- }
-
- return true;
- }
-
- /** @throws \JsonException */
- protected function sameValue(Node $value1, Node $value2): bool
- {
- return Printer::doPrint($value1) === Printer::doPrint($value2);
- }
-
- /**
- * Two types conflict if both types could not apply to a value simultaneously.
- *
- * Composite types are ignored as their individual field types will be compared
- * later recursively. However, List and Non-Null types must match.
- */
- protected function doTypesConflict(Type $type1, Type $type2): bool
- {
- if ($type1 instanceof ListOfType) {
- return $type2 instanceof ListOfType
- ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
- : true;
- }
-
- if ($type2 instanceof ListOfType) {
- return true;
- }
-
- if ($type1 instanceof NonNull) {
- return $type2 instanceof NonNull
- ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
- : true;
- }
-
- if ($type2 instanceof NonNull) {
- return true;
- }
-
- if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
- return $type1 !== $type2;
- }
-
- return false;
- }
-
- /**
- * Find all conflicts found between two selection sets, including those found
- * via spreading in fragments. Called when determining if conflicts exist
- * between the sub-fields of two overlapping fields.
- *
- * @throws \Exception
- *
- * @return array<int, Conflict>
- */
- protected function findConflictsBetweenSubSelectionSets(
- QueryValidationContext $context,
- bool $areMutuallyExclusive,
- ?Type $parentType1,
- SelectionSetNode $selectionSet1,
- ?Type $parentType2,
- SelectionSetNode $selectionSet2
- ): array {
- $conflicts = [];
-
- [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames(
- $context,
- $parentType1,
- $selectionSet1
- );
- [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames(
- $context,
- $parentType2,
- $selectionSet2
- );
-
- // (H) First, collect all conflicts between these two collections of field.
- $this->collectConflictsBetween(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fieldMap1,
- $fieldMap2
- );
-
- // (I) Then collect conflicts between the first collection of fields and
- // those referenced by each fragment name associated with the second.
- $fragmentNames2Length = count($fragmentNames2);
- if ($fragmentNames2Length !== 0) {
- $comparedFragments = [];
- for ($j = 0; $j < $fragmentNames2Length; ++$j) {
- $this->collectConflictsBetweenFieldsAndFragment(
- $context,
- $conflicts,
- $comparedFragments,
- $areMutuallyExclusive,
- $fieldMap1,
- $fragmentNames2[$j]
- );
- }
- }
-
- // (I) Then collect conflicts between the second collection of fields and
- // those referenced by each fragment name associated with the first.
- $fragmentNames1Length = count($fragmentNames1);
- if ($fragmentNames1Length !== 0) {
- $comparedFragments = [];
- for ($i = 0; $i < $fragmentNames1Length; ++$i) {
- $this->collectConflictsBetweenFieldsAndFragment(
- $context,
- $conflicts,
- $comparedFragments,
- $areMutuallyExclusive,
- $fieldMap2,
- $fragmentNames1[$i]
- );
- }
- }
-
- // (J) Also collect conflicts between any fragment names by the first and
- // fragment names by the second. This compares each item in the first set of
- // names to each item in the second set of names.
- for ($i = 0; $i < $fragmentNames1Length; ++$i) {
- for ($j = 0; $j < $fragmentNames2Length; ++$j) {
- $this->collectConflictsBetweenFragments(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fragmentNames1[$i],
- $fragmentNames2[$j]
- );
- }
- }
-
- return $conflicts;
- }
-
- /**
- * Collect all Conflicts between two collections of fields. This is similar to,
- * but different from the `collectConflictsWithin` function above. This check
- * assumes that `collectConflictsWithin` has already been called on each
- * provided collection of fields. This is true because this validator traverses
- * each individual selection set.
- *
- * @phpstan-param array<int, Conflict> $conflicts
- * @phpstan-param FieldMap $fieldMap1
- * @phpstan-param FieldMap $fieldMap2
- *
- * @throws \Exception
- */
- protected function collectConflictsBetween(
- QueryValidationContext $context,
- array &$conflicts,
- bool $parentFieldsAreMutuallyExclusive,
- array $fieldMap1,
- array $fieldMap2
- ): void {
- // A field map is a keyed collection, where each key represents a response
- // name and the value at that key is a list of all fields which provide that
- // response name. For any response name which appears in both provided field
- // maps, each field from the first field map must be compared to every field
- // in the second field map to find potential conflicts.
- foreach ($fieldMap1 as $responseName => $fields1) {
- if (! isset($fieldMap2[$responseName])) {
- continue;
- }
-
- $fields2 = $fieldMap2[$responseName];
- $fields1Length = count($fields1);
- $fields2Length = count($fields2);
- for ($i = 0; $i < $fields1Length; ++$i) {
- for ($j = 0; $j < $fields2Length; ++$j) {
- $conflict = $this->findConflict(
- $context,
- $parentFieldsAreMutuallyExclusive,
- $responseName,
- $fields1[$i],
- $fields2[$j]
- );
- if ($conflict !== null) {
- $conflicts[] = $conflict;
- }
- }
- }
- }
- }
-
- /**
- * Collect all conflicts found between a set of fields and a fragment reference
- * including via spreading in any nested fragments.
- *
- * @param array<string, true> $comparedFragments
- *
- * @phpstan-param array<int, Conflict> $conflicts
- * @phpstan-param FieldMap $fieldMap
- *
- * @throws \Exception
- */
- protected function collectConflictsBetweenFieldsAndFragment(
- QueryValidationContext $context,
- array &$conflicts,
- array &$comparedFragments,
- bool $areMutuallyExclusive,
- array $fieldMap,
- string $fragmentName
- ): void {
- if (isset($comparedFragments[$fragmentName])) {
- return;
- }
-
- $comparedFragments[$fragmentName] = true;
-
- $fragment = $context->getFragment($fragmentName);
- if ($fragment === null) {
- return;
- }
-
- [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
- $context,
- $fragment
- );
-
- if ($fieldMap === $fieldMap2) {
- return;
- }
-
- // (D) First collect any conflicts between the provided collection of fields
- // and the collection of fields represented by the given fragment.
- $this->collectConflictsBetween(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fieldMap,
- $fieldMap2
- );
-
- // (E) Then collect any conflicts between the provided collection of fields
- // and any fragment names found in the given fragment.
- $fragmentNames2Length = count($fragmentNames2);
- for ($i = 0; $i < $fragmentNames2Length; ++$i) {
- $this->collectConflictsBetweenFieldsAndFragment(
- $context,
- $conflicts,
- $comparedFragments,
- $areMutuallyExclusive,
- $fieldMap,
- $fragmentNames2[$i]
- );
- }
- }
-
- /**
- * Given a reference to a fragment, return the represented collection of fields
- * as well as a list of nested fragment names referenced via fragment spreads.
- *
- * @throws \Exception
- *
- * @phpstan-return array{FieldMap, array<int, string>}
- */
- protected function getReferencedFieldsAndFragmentNames(
- QueryValidationContext $context,
- FragmentDefinitionNode $fragment
- ): array {
- // Short-circuit building a type from the AST if possible.
- if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) {
- return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet];
- }
-
- $fragmentType = AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition);
-
- return $this->getFieldsAndFragmentNames(
- $context,
- $fragmentType,
- $fragment->selectionSet
- );
- }
-
- /**
- * Collect all conflicts found between two fragments, including via spreading in
- * any nested fragments.
- *
- * @phpstan-param array<int, Conflict> $conflicts
- *
- * @throws \Exception
- */
- protected function collectConflictsBetweenFragments(
- QueryValidationContext $context,
- array &$conflicts,
- bool $areMutuallyExclusive,
- string $fragmentName1,
- string $fragmentName2
- ): void {
- // No need to compare a fragment to itself.
- if ($fragmentName1 === $fragmentName2) {
- return;
- }
-
- // Memoize so two fragments are not compared for conflicts more than once.
- if (
- $this->comparedFragmentPairs->has(
- $fragmentName1,
- $fragmentName2,
- $areMutuallyExclusive
- )
- ) {
- return;
- }
-
- $this->comparedFragmentPairs->add(
- $fragmentName1,
- $fragmentName2,
- $areMutuallyExclusive
- );
-
- $fragment1 = $context->getFragment($fragmentName1);
- $fragment2 = $context->getFragment($fragmentName2);
- if ($fragment1 === null || $fragment2 === null) {
- return;
- }
-
- [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames(
- $context,
- $fragment1
- );
- [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
- $context,
- $fragment2
- );
-
- // (F) First, collect all conflicts between these two collections of fields
- // (not including any nested fragments).
- $this->collectConflictsBetween(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fieldMap1,
- $fieldMap2
- );
-
- // (G) Then collect conflicts between the first fragment and any nested
- // fragments spread in the second fragment.
- $fragmentNames2Length = count($fragmentNames2);
- for ($j = 0; $j < $fragmentNames2Length; ++$j) {
- $this->collectConflictsBetweenFragments(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fragmentName1,
- $fragmentNames2[$j]
- );
- }
-
- // (G) Then collect conflicts between the second fragment and any nested
- // fragments spread in the first fragment.
- $fragmentNames1Length = count($fragmentNames1);
- for ($i = 0; $i < $fragmentNames1Length; ++$i) {
- $this->collectConflictsBetweenFragments(
- $context,
- $conflicts,
- $areMutuallyExclusive,
- $fragmentNames1[$i],
- $fragmentName2
- );
- }
- }
-
- /**
- * Merge Conflicts between two sub-fields into a single Conflict.
- *
- * @phpstan-param array<int, Conflict> $conflicts
- *
- * @phpstan-return Conflict|null
- */
- protected function subfieldConflicts(
- array $conflicts,
- string $responseName,
- FieldNode $ast1,
- FieldNode $ast2
- ): ?array {
- if ($conflicts === []) {
- return null;
- }
-
- $reasons = [];
- foreach ($conflicts as $conflict) {
- $reasons[] = $conflict[0];
- }
-
- $fields1 = [$ast1];
- foreach ($conflicts as $conflict) {
- foreach ($conflict[1] as $field) {
- $fields1[] = $field;
- }
- }
-
- $fields2 = [$ast2];
- foreach ($conflicts as $conflict) {
- foreach ($conflict[2] as $field) {
- $fields2[] = $field;
- }
- }
-
- return [
- [
- $responseName,
- $reasons,
- ],
- $fields1,
- $fields2,
- ];
- }
-
- /**
- * @param string|array $reasonOrReasons
- *
- * @phpstan-param ReasonOrReasons $reasonOrReasons
- */
- public static function fieldsConflictMessage(string $responseName, $reasonOrReasons): string
- {
- $reasonMessage = static::reasonMessage($reasonOrReasons);
-
- return "Fields \"{$responseName}\" conflict because {$reasonMessage}. Use different aliases on the fields to fetch both if this was intentional.";
- }
-
- /**
- * @param string|array $reasonOrReasons
- *
- * @phpstan-param ReasonOrReasons $reasonOrReasons
- */
- public static function reasonMessage($reasonOrReasons): string
- {
- if (is_array($reasonOrReasons)) {
- $reasons = array_map(
- static function (array $reason): string {
- [$responseName, $subReason] = $reason;
- $reasonMessage = static::reasonMessage($subReason);
-
- return "subfields \"{$responseName}\" conflict because {$reasonMessage}";
- },
- $reasonOrReasons
- );
-
- return implode(' and ', $reasons);
- }
-
- return $reasonOrReasons;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php
deleted file mode 100644
index 9140f64b5e3..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php
+++ /dev/null
@@ -1,165 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class PossibleFragmentSpreads extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context): void {
- $fragType = $context->getType();
- $parentType = $context->getParentType();
-
- if (
- ! $fragType instanceof CompositeType
- || ! $parentType instanceof CompositeType
- || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
- ) {
- return;
- }
-
- $context->reportError(new Error(
- static::typeIncompatibleAnonSpreadMessage($parentType->toString(), $fragType->toString()),
- [$node]
- ));
- },
- NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context): void {
- $fragName = $node->name->value;
- $fragType = $this->getFragmentType($context, $fragName);
- $parentType = $context->getParentType();
-
- if (
- $fragType === null
- || $parentType === null
- || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
- ) {
- return;
- }
-
- $context->reportError(new Error(
- static::typeIncompatibleSpreadMessage($fragName, $parentType->toString(), $fragType->toString()),
- [$node]
- ));
- },
- ];
- }
-
- /**
- * @param CompositeType&Type $fragType
- * @param CompositeType&Type $parentType
- *
- * @throws InvariantViolation
- */
- protected function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType): bool
- {
- // Checking in the order of the most frequently used scenarios:
- // Parent type === fragment type
- if ($parentType === $fragType) {
- return true;
- }
-
- // Parent type is interface or union, fragment type is object type
- if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
- return $schema->isSubType($parentType, $fragType);
- }
-
- // Parent type is object type, fragment type is interface (or rather rare - union)
- if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
- return $schema->isSubType($fragType, $parentType);
- }
-
- // Both are object types:
- if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) {
- return $parentType === $fragType;
- }
-
- // Both are interfaces
- // This case may be assumed valid only when implementations of two interfaces intersect
- // But we don't have information about all implementations at runtime
- // (getting this information via $schema->getPossibleTypes() requires scanning through whole schema
- // which is very costly to do at each request due to PHP "shared nothing" architecture)
- //
- // So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution
- // See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602
- if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) {
- return true;
-
- // Note that there is one case when we do have information about all implementations:
- // When schema descriptor is defined ($schema->hasDescriptor())
- // BUT we must avoid situation when some query that worked in development had suddenly stopped
- // working in production. So staying consistent and always validate.
- }
-
- // Interface within union
- if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) {
- foreach ($parentType->getTypes() as $type) {
- if ($type->implementsInterface($fragType)) {
- return true;
- }
- }
- }
-
- if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) {
- foreach ($fragType->getTypes() as $type) {
- if ($type->implementsInterface($parentType)) {
- return true;
- }
- }
- }
-
- if ($parentType instanceof UnionType && $fragType instanceof UnionType) {
- foreach ($fragType->getTypes() as $type) {
- if ($parentType->isPossibleType($type)) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- public static function typeIncompatibleAnonSpreadMessage(string $parentType, string $fragType): string
- {
- return "Fragment cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\".";
- }
-
- /**
- * @throws \Exception
- *
- * @return (CompositeType&Type)|null
- */
- protected function getFragmentType(QueryValidationContext $context, string $name): ?Type
- {
- $frag = $context->getFragment($name);
- if ($frag === null) {
- return null;
- }
-
- $type = AST::typeFromAST([$context->getSchema(), 'getType'], $frag->typeCondition);
-
- return $type instanceof CompositeType
- ? $type
- : null;
- }
-
- public static function typeIncompatibleSpreadMessage(string $fragName, string $parentType, string $fragType): string
- {
- return "Fragment \"{$fragName}\" cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php
deleted file mode 100644
index 16402c0f958..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php
+++ /dev/null
@@ -1,163 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Possible type extensions.
- *
- * A type extension is only valid if the type is defined and has the same kind.
- */
-class PossibleTypeExtensions extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $schema = $context->getSchema();
-
- /** @var array<string, TypeDefinitionNode&Node> $definedTypes */
- $definedTypes = [];
- foreach ($context->getDocument()->definitions as $def) {
- if ($def instanceof TypeDefinitionNode) {
- $name = $def->getName()->value;
- $definedTypes[$name] = $def;
- }
- }
-
- $checkTypeExtension = static function ($node) use ($context, $schema, &$definedTypes): ?VisitorOperation {
- $typeName = $node->name->value;
- $defNode = $definedTypes[$typeName] ?? null;
- $existingType = $schema !== null
- ? $schema->getType($typeName)
- : null;
-
- $expectedKind = null;
- if ($defNode !== null) {
- $expectedKind = self::defKindToExtKind($defNode->kind);
- } elseif ($existingType !== null) {
- $expectedKind = self::typeToExtKind($existingType);
- }
-
- if ($expectedKind !== null) {
- if ($expectedKind !== $node->kind) {
- $kindStr = self::extensionKindToTypeName($node->kind);
- $context->reportError(
- new Error(
- "Cannot extend non-{$kindStr} type \"{$typeName}\".",
- $defNode !== null
- ? [$defNode, $node]
- : $node,
- ),
- );
- }
- } else {
- $existingTypesMap = $schema !== null
- ? $schema->getTypeMap()
- : [];
- $allTypeNames = [
- ...array_keys($definedTypes),
- ...array_keys($existingTypesMap),
- ];
- $suggestedTypes = Utils::suggestionList($typeName, $allTypeNames);
- $didYouMean = $suggestedTypes === []
- ? ''
- : ' Did you mean ' . Utils::quotedOrList($suggestedTypes) . '?';
- $context->reportError(
- new Error(
- "Cannot extend type \"{$typeName}\" because it is not defined.{$didYouMean}",
- $node->name,
- ),
- );
- }
-
- return null;
- };
-
- return [
- NodeKind::SCALAR_TYPE_EXTENSION => $checkTypeExtension,
- NodeKind::OBJECT_TYPE_EXTENSION => $checkTypeExtension,
- NodeKind::INTERFACE_TYPE_EXTENSION => $checkTypeExtension,
- NodeKind::UNION_TYPE_EXTENSION => $checkTypeExtension,
- NodeKind::ENUM_TYPE_EXTENSION => $checkTypeExtension,
- NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkTypeExtension,
- ];
- }
-
- /** @throws InvariantViolation */
- private static function defKindToExtKind(string $kind): string
- {
- switch ($kind) {
- case NodeKind::SCALAR_TYPE_DEFINITION:
- return NodeKind::SCALAR_TYPE_EXTENSION;
- case NodeKind::OBJECT_TYPE_DEFINITION:
- return NodeKind::OBJECT_TYPE_EXTENSION;
- case NodeKind::INTERFACE_TYPE_DEFINITION:
- return NodeKind::INTERFACE_TYPE_EXTENSION;
- case NodeKind::UNION_TYPE_DEFINITION:
- return NodeKind::UNION_TYPE_EXTENSION;
- case NodeKind::ENUM_TYPE_DEFINITION:
- return NodeKind::ENUM_TYPE_EXTENSION;
- case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
- return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
- default:
- throw new InvariantViolation("Unexpected definition kind: {$kind}.");
- }
- }
-
- /** @throws InvariantViolation */
- private static function typeToExtKind(NamedType $type): string
- {
- switch (true) {
- case $type instanceof ScalarType:
- return NodeKind::SCALAR_TYPE_EXTENSION;
- case $type instanceof ObjectType:
- return NodeKind::OBJECT_TYPE_EXTENSION;
- case $type instanceof InterfaceType:
- return NodeKind::INTERFACE_TYPE_EXTENSION;
- case $type instanceof UnionType:
- return NodeKind::UNION_TYPE_EXTENSION;
- case $type instanceof EnumType:
- return NodeKind::ENUM_TYPE_EXTENSION;
- case $type instanceof InputObjectType:
- return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
- default:
- $unexpectedType = Utils::printSafe($type);
- throw new InvariantViolation("Unexpected type: {$unexpectedType}.");
- }
- }
-
- /** @throws InvariantViolation */
- private static function extensionKindToTypeName(string $kind): string
- {
- switch ($kind) {
- case NodeKind::SCALAR_TYPE_EXTENSION:
- return 'scalar';
- case NodeKind::OBJECT_TYPE_EXTENSION:
- return 'object';
- case NodeKind::INTERFACE_TYPE_EXTENSION:
- return 'interface';
- case NodeKind::UNION_TYPE_EXTENSION:
- return 'union';
- case NodeKind::ENUM_TYPE_EXTENSION:
- return 'enum';
- case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
- return 'input object';
- default:
- throw new InvariantViolation("Unexpected extension kind: {$kind}.");
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php
deleted file mode 100644
index 0974950a56b..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class ProvidedRequiredArguments extends ValidationRule
-{
- /** @throws \Exception */
- public function getVisitor(QueryValidationContext $context): array
- {
- $providedRequiredArgumentsOnDirectives = new ProvidedRequiredArgumentsOnDirectives();
-
- return $providedRequiredArgumentsOnDirectives->getVisitor($context) + [
- NodeKind::FIELD => [
- 'leave' => static function (FieldNode $fieldNode) use ($context): ?VisitorOperation {
- $fieldDef = $context->getFieldDef();
-
- if ($fieldDef === null) {
- return Visitor::skipNode();
- }
-
- $argNodes = $fieldNode->arguments;
-
- $argNodeMap = [];
- foreach ($argNodes as $argNode) {
- $argNodeMap[$argNode->name->value] = $argNode;
- }
-
- foreach ($fieldDef->args as $argDef) {
- $argNode = $argNodeMap[$argDef->name] ?? null;
- if ($argNode === null && $argDef->isRequired()) {
- $context->reportError(new Error(
- static::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()->toString()),
- [$fieldNode]
- ));
- }
- }
-
- return null;
- },
- ],
- ];
- }
-
- public static function missingFieldArgMessage(string $fieldName, string $argName, string $type): string
- {
- return "Field \"{$fieldName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
deleted file mode 100644
index 1ee38758f2e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * Provided required arguments on directives.
- *
- * A directive is only valid if all required (non-null without a
- * default value) field arguments have been provided.
- *
- * @phpstan-import-type VisitorArray from Visitor
- */
-class ProvidedRequiredArgumentsOnDirectives extends ValidationRule
-{
- public static function missingDirectiveArgMessage(string $directiveName, string $argName, string $type): string
- {
- return "Directive \"@{$directiveName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided.";
- }
-
- /** @throws \Exception */
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @throws \Exception */
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /**
- * @throws \Exception
- * @throws \InvalidArgumentException
- * @throws \ReflectionException
- * @throws Error
- * @throws InvariantViolation
- *
- * @phpstan-return VisitorArray
- */
- public function getASTVisitor(ValidationContext $context): array
- {
- $requiredArgsMap = [];
- $schema = $context->getSchema();
- $definedDirectives = $schema === null
- ? Directive::getInternalDirectives()
- : $schema->getDirectives();
-
- foreach ($definedDirectives as $directive) {
- $directiveArgs = [];
- foreach ($directive->args as $arg) {
- if ($arg->isRequired()) {
- $directiveArgs[$arg->name] = $arg;
- }
- }
-
- $requiredArgsMap[$directive->name] = $directiveArgs;
- }
-
- $astDefinition = $context->getDocument()->definitions;
- foreach ($astDefinition as $def) {
- if ($def instanceof DirectiveDefinitionNode) {
- $arguments = $def->arguments;
-
- $requiredArgs = [];
- foreach ($arguments as $argument) {
- if ($argument->type instanceof NonNullTypeNode && ! isset($argument->defaultValue)) {
- $requiredArgs[$argument->name->value] = $argument;
- }
- }
-
- $requiredArgsMap[$def->name->value] = $requiredArgs;
- }
- }
-
- return [
- NodeKind::DIRECTIVE => [
- // Validate on leave to allow for deeper errors to appear first.
- 'leave' => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context): ?string {
- $directiveName = $directiveNode->name->value;
- $requiredArgs = $requiredArgsMap[$directiveName] ?? null;
- if ($requiredArgs === null || $requiredArgs === []) {
- return null;
- }
-
- $argNodeMap = [];
- foreach ($directiveNode->arguments as $arg) {
- $argNodeMap[$arg->name->value] = $arg;
- }
-
- foreach ($requiredArgs as $argName => $arg) {
- if (! isset($argNodeMap[$argName])) {
- $argType = $arg instanceof Argument
- ? $arg->getType()->toString()
- : Printer::doPrint($arg->type);
-
- $context->reportError(
- new Error(static::missingDirectiveArgMessage($directiveName, $argName, $argType), [$directiveNode])
- );
- }
- }
-
- return null;
- },
- ],
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php
deleted file mode 100644
index aa25931aa97..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php
+++ /dev/null
@@ -1,302 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * @phpstan-import-type ASTAndDefs from QuerySecurityRule
- */
-class QueryComplexity extends QuerySecurityRule
-{
- protected int $maxQueryComplexity;
-
- protected int $queryComplexity;
-
- /** @var array<string, mixed> */
- protected array $rawVariableValues = [];
-
- /** @var NodeList<VariableDefinitionNode> */
- protected NodeList $variableDefs;
-
- /** @phpstan-var ASTAndDefs */
- protected \ArrayObject $fieldNodeAndDefs;
-
- protected QueryValidationContext $context;
-
- /** @throws \InvalidArgumentException */
- public function __construct(int $maxQueryComplexity)
- {
- $this->setMaxQueryComplexity($maxQueryComplexity);
- }
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->queryComplexity = 0;
- $this->context = $context;
- $this->variableDefs = new NodeList([]);
- $this->fieldNodeAndDefs = new \ArrayObject();
-
- return $this->invokeIfNeeded(
- $context,
- [
- NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void {
- $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
- $context,
- $context->getParentType(),
- $selectionSet,
- null,
- $this->fieldNodeAndDefs
- );
- },
- NodeKind::VARIABLE_DEFINITION => function ($def): VisitorOperation {
- $this->variableDefs[] = $def;
-
- return Visitor::skipNode();
- },
- NodeKind::DOCUMENT => [
- 'leave' => function (DocumentNode $document) use ($context): void {
- $errors = $context->getErrors();
-
- if ($errors !== []) {
- return;
- }
-
- if ($this->maxQueryComplexity === self::DISABLED) {
- return;
- }
-
- foreach ($document->definitions as $definition) {
- if (! $definition instanceof OperationDefinitionNode) {
- continue;
- }
-
- $this->queryComplexity = $this->fieldComplexity($definition->selectionSet);
-
- if ($this->queryComplexity > $this->maxQueryComplexity) {
- $context->reportError(
- new Error(static::maxQueryComplexityErrorMessage(
- $this->maxQueryComplexity,
- $this->queryComplexity
- ))
- );
-
- return;
- }
- }
- },
- ],
- ]
- );
- }
-
- /** @throws \Exception */
- protected function fieldComplexity(SelectionSetNode $selectionSet): int
- {
- $complexity = 0;
-
- foreach ($selectionSet->selections as $selection) {
- $complexity += $this->nodeComplexity($selection);
- }
-
- return $complexity;
- }
-
- /** @throws \Exception */
- protected function nodeComplexity(SelectionNode $node): int
- {
- switch (true) {
- case $node instanceof FieldNode:
- // Exclude __schema field and all nested content from complexity calculation
- if ($node->name->value === Introspection::SCHEMA_FIELD_NAME) {
- return 0;
- }
-
- if ($this->directiveExcludesField($node)) {
- return 0;
- }
-
- $childrenComplexity = isset($node->selectionSet)
- ? $this->fieldComplexity($node->selectionSet)
- : 0;
-
- $fieldDef = $this->fieldDefinition($node);
- if ($fieldDef instanceof FieldDefinition && $fieldDef->complexityFn !== null) {
- $fieldArguments = $this->buildFieldArguments($node);
-
- return ($fieldDef->complexityFn)($childrenComplexity, $fieldArguments);
- }
-
- return $childrenComplexity + 1;
-
- case $node instanceof InlineFragmentNode:
- return $this->fieldComplexity($node->selectionSet);
-
- case $node instanceof FragmentSpreadNode:
- $fragment = $this->getFragment($node);
-
- if ($fragment !== null) {
- return $this->fieldComplexity($fragment->selectionSet);
- }
- }
-
- return 0;
- }
-
- protected function fieldDefinition(FieldNode $field): ?FieldDefinition
- {
- foreach ($this->fieldNodeAndDefs[$this->getFieldName($field)] ?? [] as [$node, $def]) {
- if ($node === $field) {
- return $def;
- }
- }
-
- return null;
- }
-
- /**
- * Will the given field be executed at all, given the directives placed upon it?
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- */
- protected function directiveExcludesField(FieldNode $node): bool
- {
- foreach ($node->directives as $directiveNode) {
- if ($directiveNode->name->value === Directive::DEPRECATED_NAME) {
- return false;
- }
-
- [$errors, $variableValues] = Values::getVariableValues(
- $this->context->getSchema(),
- $this->variableDefs,
- $this->getRawVariableValues()
- );
- if ($errors !== null && $errors !== []) {
- throw new Error(implode("\n\n", array_map(static fn (Error $error): string => $error->getMessage(), $errors)));
- }
-
- if ($directiveNode->name->value === Directive::INCLUDE_NAME) {
- $includeArguments = Values::getArgumentValues(
- Directive::includeDirective(),
- $directiveNode,
- $variableValues
- );
- assert(is_bool($includeArguments['if']), 'ensured by query validation');
-
- return ! $includeArguments['if'];
- }
-
- if ($directiveNode->name->value === Directive::SKIP_NAME) {
- $skipArguments = Values::getArgumentValues(
- Directive::skipDirective(),
- $directiveNode,
- $variableValues
- );
- assert(is_bool($skipArguments['if']), 'ensured by query validation');
-
- return $skipArguments['if'];
- }
- }
-
- return false;
- }
-
- /** @return array<string, mixed> */
- public function getRawVariableValues(): array
- {
- return $this->rawVariableValues;
- }
-
- /** @param array<string, mixed>|null $rawVariableValues */
- public function setRawVariableValues(?array $rawVariableValues = null): void
- {
- $this->rawVariableValues = $rawVariableValues ?? [];
- }
-
- /**
- * @throws \Exception
- * @throws Error
- *
- * @return array<string, mixed>
- */
- protected function buildFieldArguments(FieldNode $node): array
- {
- $rawVariableValues = $this->getRawVariableValues();
- $fieldDef = $this->fieldDefinition($node);
-
- /** @var array<string, mixed> $args */
- $args = [];
-
- if ($fieldDef instanceof FieldDefinition) {
- [$errors, $variableValues] = Values::getVariableValues(
- $this->context->getSchema(),
- $this->variableDefs,
- $rawVariableValues
- );
-
- if (is_array($errors) && $errors !== []) {
- throw new Error(implode("\n\n", array_map(static fn ($error) => $error->getMessage(), $errors)));
- }
-
- $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
- }
-
- return $args;
- }
-
- public function getMaxQueryComplexity(): int
- {
- return $this->maxQueryComplexity;
- }
-
- /**
- * Complexity of the first operation exceeding the defined limit, or, in case no operation
- * exceeds the limit, complexity of the last defined operation.
- */
- public function getQueryComplexity(): int
- {
- return $this->queryComplexity;
- }
-
- /**
- * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
- *
- * @throws \InvalidArgumentException
- */
- public function setMaxQueryComplexity(int $maxQueryComplexity): void
- {
- $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
-
- $this->maxQueryComplexity = $maxQueryComplexity;
- }
-
- public static function maxQueryComplexityErrorMessage(int $max, int $count): string
- {
- return "Max query complexity should be {$max} but got {$count}.";
- }
-
- protected function isEnabled(): bool
- {
- return $this->maxQueryComplexity !== self::DISABLED;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php
deleted file mode 100644
index 5a21d715ed6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class QueryDepth extends QuerySecurityRule
-{
- /** @var array<string, bool> Fragment names which are already calculated in recursion */
- protected array $calculatedFragments = [];
-
- protected int $maxQueryDepth;
-
- /** @throws \InvalidArgumentException */
- public function __construct(int $maxQueryDepth)
- {
- $this->setMaxQueryDepth($maxQueryDepth);
- }
-
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->invokeIfNeeded(
- $context,
- [
- NodeKind::OPERATION_DEFINITION => [
- 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context): void {
- $maxDepth = $this->fieldDepth($operationDefinition);
-
- if ($maxDepth <= $this->maxQueryDepth) {
- return;
- }
-
- $context->reportError(
- new Error(static::maxQueryDepthErrorMessage($this->maxQueryDepth, $maxDepth))
- );
- },
- ],
- ]
- );
- }
-
- /** @param OperationDefinitionNode|FieldNode|InlineFragmentNode|FragmentDefinitionNode $node */
- protected function fieldDepth(Node $node, int $depth = 0, int $maxDepth = 0): int
- {
- if ($node->selectionSet instanceof SelectionSetNode) {
- foreach ($node->selectionSet->selections as $childNode) {
- $maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth);
- }
- }
-
- return $maxDepth;
- }
-
- protected function nodeDepth(Node $node, int $depth = 0, int $maxDepth = 0): int
- {
- switch (true) {
- case $node instanceof FieldNode:
- // node has children?
- if ($node->selectionSet !== null) {
- // update maxDepth if needed
- if ($depth > $maxDepth) {
- $maxDepth = $depth;
- }
-
- $maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth);
- }
-
- break;
-
- case $node instanceof InlineFragmentNode:
- $maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
-
- break;
-
- case $node instanceof FragmentSpreadNode:
- $fragment = $this->getFragment($node);
-
- if ($fragment !== null) {
- $name = $fragment->name->value;
- if (isset($this->calculatedFragments[$name])) {
- return $this->maxQueryDepth + 1;
- }
-
- $this->calculatedFragments[$name] = true;
- $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
- unset($this->calculatedFragments[$name]);
- }
-
- break;
- }
-
- return $maxDepth;
- }
-
- public function getMaxQueryDepth(): int
- {
- return $this->maxQueryDepth;
- }
-
- /**
- * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
- *
- * @throws \InvalidArgumentException
- */
- public function setMaxQueryDepth(int $maxQueryDepth): void
- {
- $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
-
- $this->maxQueryDepth = $maxQueryDepth;
- }
-
- public static function maxQueryDepthErrorMessage(int $max, int $count): string
- {
- return "Max query depth should be {$max} but got {$count}.";
- }
-
- protected function isEnabled(): bool
- {
- return $this->maxQueryDepth !== self::DISABLED;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php
deleted file mode 100644
index 69f761ebb81..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php
+++ /dev/null
@@ -1,184 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * @see Visitor, FieldDefinition
- *
- * @phpstan-import-type VisitorArray from Visitor
- *
- * @phpstan-type ASTAndDefs \ArrayObject<string, \ArrayObject<int, array{FieldNode, FieldDefinition|null}>>
- */
-abstract class QuerySecurityRule extends ValidationRule
-{
- public const DISABLED = 0;
-
- /** @var array<string, FragmentDefinitionNode> */
- protected array $fragments = [];
-
- /** @throws \InvalidArgumentException */
- protected function checkIfGreaterOrEqualToZero(string $name, int $value): void
- {
- if ($value < 0) {
- throw new \InvalidArgumentException("\${$name} argument must be greater or equal to 0.");
- }
- }
-
- protected function getFragment(FragmentSpreadNode $fragmentSpread): ?FragmentDefinitionNode
- {
- return $this->fragments[$fragmentSpread->name->value] ?? null;
- }
-
- /** @return array<string, FragmentDefinitionNode> */
- protected function getFragments(): array
- {
- return $this->fragments;
- }
-
- /**
- * @phpstan-param VisitorArray $validators
- *
- * @phpstan-return VisitorArray
- */
- protected function invokeIfNeeded(QueryValidationContext $context, array $validators): array
- {
- if (! $this->isEnabled()) {
- return [];
- }
-
- $this->gatherFragmentDefinition($context);
-
- return $validators;
- }
-
- abstract protected function isEnabled(): bool;
-
- protected function gatherFragmentDefinition(QueryValidationContext $context): void
- {
- // Gather all the fragment definition.
- // Importantly this does not include inline fragments.
- $definitions = $context->getDocument()->definitions;
- foreach ($definitions as $node) {
- if ($node instanceof FragmentDefinitionNode) {
- $this->fragments[$node->name->value] = $node;
- }
- }
- }
-
- /**
- * Given a selectionSet, adds all fields in that selection to
- * the passed in map of fields, and returns it at the end.
- *
- * Note: This is not the same as execution's collectFields because at static
- * time we do not know what object type will be used, so we unconditionally
- * spread in all fragments.
- *
- * @see \Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
- *
- * @param \ArrayObject<string, true>|null $visitedFragmentNames
- *
- * @phpstan-param ASTAndDefs|null $astAndDefs
- *
- * @throws \Exception
- * @throws \ReflectionException
- * @throws InvariantViolation
- *
- * @phpstan-return ASTAndDefs
- */
- protected function collectFieldASTsAndDefs(
- QueryValidationContext $context,
- ?Type $parentType,
- SelectionSetNode $selectionSet,
- ?\ArrayObject $visitedFragmentNames = null,
- ?\ArrayObject $astAndDefs = null
- ): \ArrayObject {
- $visitedFragmentNames ??= new \ArrayObject();
- $astAndDefs ??= new \ArrayObject();
-
- foreach ($selectionSet->selections as $selection) {
- if ($selection instanceof FieldNode) {
- $fieldName = $selection->name->value;
-
- $fieldDef = null;
- if ($parentType instanceof HasFieldsType) {
- $schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
- $typeMetaFieldDef = Introspection::typeMetaFieldDef();
- $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
-
- $queryType = $context->getSchema()->getQueryType();
-
- if ($fieldName === $schemaMetaFieldDef->name && $queryType === $parentType) {
- $fieldDef = $schemaMetaFieldDef;
- } elseif ($fieldName === $typeMetaFieldDef->name && $queryType === $parentType) {
- $fieldDef = $typeMetaFieldDef;
- } elseif ($fieldName === $typeNameMetaFieldDef->name) {
- $fieldDef = $typeNameMetaFieldDef;
- } elseif ($parentType->hasField($fieldName)) {
- $fieldDef = $parentType->getField($fieldName);
- }
- }
-
- $responseName = $this->getFieldName($selection);
- $responseContext = $astAndDefs[$responseName] ??= new \ArrayObject();
- $responseContext[] = [$selection, $fieldDef];
- } elseif ($selection instanceof InlineFragmentNode) {
- $typeCondition = $selection->typeCondition;
- $fragmentParentType = $typeCondition === null
- ? $parentType
- : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);
- $astAndDefs = $this->collectFieldASTsAndDefs(
- $context,
- $fragmentParentType,
- $selection->selectionSet,
- $visitedFragmentNames,
- $astAndDefs
- );
- } elseif ($selection instanceof FragmentSpreadNode) {
- $fragName = $selection->name->value;
-
- if (isset($visitedFragmentNames[$fragName])) {
- continue;
- }
- $visitedFragmentNames[$fragName] = true;
-
- $fragment = $context->getFragment($fragName);
- if ($fragment === null) {
- continue;
- }
-
- $astAndDefs = $this->collectFieldASTsAndDefs(
- $context,
- AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition),
- $fragment->selectionSet,
- $visitedFragmentNames,
- $astAndDefs
- );
- }
- }
-
- return $astAndDefs;
- }
-
- protected function getFieldName(FieldNode $node): string
- {
- $fieldName = $node->name->value;
-
- return $node->alias === null
- ? $fieldName
- : $node->alias->value;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php
deleted file mode 100644
index 2b7c66bf837..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class ScalarLeafs extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::FIELD => static function (FieldNode $node) use ($context): void {
- $type = $context->getType();
- if ($type === null) {
- return;
- }
-
- if (Type::isLeafType(Type::getNamedType($type))) {
- if ($node->selectionSet !== null) {
- $context->reportError(new Error(
- static::noSubselectionAllowedMessage($node->name->value, $type->toString()),
- [$node->selectionSet]
- ));
- }
- } elseif ($node->selectionSet === null) {
- $context->reportError(new Error(
- static::requiredSubselectionMessage($node->name->value, $type->toString()),
- [$node]
- ));
- }
- },
- ];
- }
-
- public static function noSubselectionAllowedMessage(string $field, string $type): string
- {
- return "Field \"{$field}\" of type \"{$type}\" must not have a sub selection.";
- }
-
- public static function requiredSubselectionMessage(string $field, string $type): string
- {
- return "Field \"{$field}\" of type \"{$type}\" must have a sub selection.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php
deleted file mode 100644
index e9163507b76..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class SingleFieldSubscription extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ($context): VisitorOperation {
- if ($node->operation === 'subscription') {
- $selections = $node->selectionSet->selections;
-
- if (count($selections) > 1) {
- $offendingSelections = $selections->splice(1, count($selections));
-
- $context->reportError(new Error(
- static::multipleFieldsInOperation($node->name->value ?? null),
- $offendingSelections
- ));
- }
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-
- public static function multipleFieldsInOperation(?string $operationName): string
- {
- if ($operationName === null) {
- return 'Anonymous Subscription must select only one top level field.';
- }
-
- return "Subscription \"{$operationName}\" must select only one top level field.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php
deleted file mode 100644
index 0f11d73c2ad..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Unique argument definition names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments.
- * A Automattic\WooCommerce\Vendor\GraphQL Directive is only valid if all its arguments are uniquely named.
- */
-class UniqueArgumentDefinitionNames extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $checkArgUniquenessPerField = static function ($node) use ($context): VisitorOperation {
- assert(
- $node instanceof InterfaceTypeDefinitionNode
- || $node instanceof InterfaceTypeExtensionNode
- || $node instanceof ObjectTypeDefinitionNode
- || $node instanceof ObjectTypeExtensionNode
- );
-
- foreach ($node->fields as $fieldDef) {
- self::checkArgUniqueness("{$node->name->value}.{$fieldDef->name->value}", $fieldDef->arguments, $context);
- }
-
- return Visitor::skipNode();
- };
-
- return [
- NodeKind::DIRECTIVE_DEFINITION => static fn (DirectiveDefinitionNode $node): VisitorOperation => self::checkArgUniqueness("@{$node->name->value}", $node->arguments, $context),
- NodeKind::INTERFACE_TYPE_DEFINITION => $checkArgUniquenessPerField,
- NodeKind::INTERFACE_TYPE_EXTENSION => $checkArgUniquenessPerField,
- NodeKind::OBJECT_TYPE_DEFINITION => $checkArgUniquenessPerField,
- NodeKind::OBJECT_TYPE_EXTENSION => $checkArgUniquenessPerField,
- ];
- }
-
- /** @param NodeList<InputValueDefinitionNode> $arguments */
- private static function checkArgUniqueness(string $parentName, NodeList $arguments, SDLValidationContext $context): VisitorOperation
- {
- $seenArgs = [];
- foreach ($arguments as $argument) {
- $seenArgs[$argument->name->value][] = $argument;
- }
-
- foreach ($seenArgs as $argName => $argNodes) {
- if (count($argNodes) > 1) {
- $context->reportError(
- new Error(
- "Argument \"{$parentName}({$argName}:)\" can only be defined once.",
- $argNodes,
- ),
- );
- }
- }
-
- return Visitor::skipNode();
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php
deleted file mode 100644
index b6d5134059e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * @phpstan-import-type VisitorArray from Visitor
- */
-class UniqueArgumentNames extends ValidationRule
-{
- /** @var array<string, NameNode> */
- protected array $knownArgNames;
-
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @phpstan-return VisitorArray */
- public function getASTVisitor(ValidationContext $context): array
- {
- $this->knownArgNames = [];
-
- return [
- NodeKind::FIELD => function (): void {
- $this->knownArgNames = [];
- },
- NodeKind::DIRECTIVE => function (): void {
- $this->knownArgNames = [];
- },
- NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context): VisitorOperation {
- $argName = $node->name->value;
- if (isset($this->knownArgNames[$argName])) {
- $context->reportError(new Error(
- static::duplicateArgMessage($argName),
- [$this->knownArgNames[$argName], $node->name]
- ));
- } else {
- $this->knownArgNames[$argName] = $node->name;
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-
- public static function duplicateArgMessage(string $argName): string
- {
- return "There can be only one argument named \"{$argName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php
deleted file mode 100644
index c80867ceca6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Unique directive names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all defined directives have unique names.
- */
-class UniqueDirectiveNames extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $schema = $context->getSchema();
-
- /** @var array<string, NameNode> $knownDirectiveNames */
- $knownDirectiveNames = [];
-
- return [
- NodeKind::DIRECTIVE_DEFINITION => static function ($node) use ($context, $schema, &$knownDirectiveNames): ?VisitorOperation {
- $directiveName = $node->name->value;
-
- if ($schema !== null && $schema->getDirective($directiveName) !== null) {
- $context->reportError(
- new Error(
- 'Directive "@' . $directiveName . '" already exists in the schema. It cannot be redefined.',
- $node->name,
- ),
- );
-
- return null;
- }
-
- if (isset($knownDirectiveNames[$directiveName])) {
- $context->reportError(
- new Error(
- 'There can be only one directive named "@' . $directiveName . '".',
- [
- $knownDirectiveNames[$directiveName],
- $node->name,
- ]
- ),
- );
- } else {
- $knownDirectiveNames[$directiveName] = $node->name;
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php
deleted file mode 100644
index 750a527ddb6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * Unique directive names per location.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all non-repeatable directives at
- * a given location are uniquely named.
- *
- * @phpstan-import-type VisitorArray from Visitor
- */
-class UniqueDirectivesPerLocation extends ValidationRule
-{
- /** @throws InvariantViolation */
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @throws InvariantViolation */
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /**
- * @throws InvariantViolation
- *
- * @phpstan-return VisitorArray
- */
- public function getASTVisitor(ValidationContext $context): array
- {
- /** @var array<string, true> $uniqueDirectiveMap */
- $uniqueDirectiveMap = [];
-
- $schema = $context->getSchema();
- $definedDirectives = $schema !== null
- ? $schema->getDirectives()
- : Directive::getInternalDirectives();
- foreach ($definedDirectives as $directive) {
- if (! $directive->isRepeatable) {
- $uniqueDirectiveMap[$directive->name] = true;
- }
- }
-
- $astDefinitions = $context->getDocument()->definitions;
- foreach ($astDefinitions as $definition) {
- if ($definition instanceof DirectiveDefinitionNode
- && ! $definition->repeatable
- ) {
- $uniqueDirectiveMap[$definition->name->value] = true;
- }
- }
-
- return [
- 'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context): void {
- if (! property_exists($node, 'directives')) {
- return;
- }
-
- $knownDirectives = [];
-
- foreach ($node->directives as $directive) {
- $directiveName = $directive->name->value;
-
- if (isset($uniqueDirectiveMap[$directiveName])) {
- if (isset($knownDirectives[$directiveName])) {
- $context->reportError(new Error(
- static::duplicateDirectiveMessage($directiveName),
- [$knownDirectives[$directiveName], $directive]
- ));
- } else {
- $knownDirectives[$directiveName] = $directive;
- }
- }
- }
- },
- ];
- }
-
- public static function duplicateDirectiveMessage(string $directiveName): string
- {
- return "The directive \"{$directiveName}\" can only be used once at this location.";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php
deleted file mode 100644
index c83c0eec2d6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-class UniqueEnumValueNames extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- /** @var array<string, array<string, EnumValueNode>> $knownValueNames */
- $knownValueNames = [];
-
- /**
- * @param EnumTypeDefinitionNode|EnumTypeExtensionNode $enum
- */
- $checkValueUniqueness = static function ($enum) use ($context, &$knownValueNames): VisitorOperation {
- $typeName = $enum->name->value;
-
- $schema = $context->getSchema();
- $existingType = $schema !== null
- ? $schema->getType($typeName)
- : null;
-
- $valueNodes = $enum->values;
-
- if (! isset($knownValueNames[$typeName])) {
- $knownValueNames[$typeName] = [];
- }
-
- $valueNames = &$knownValueNames[$typeName];
-
- foreach ($valueNodes as $valueDef) {
- $valueNameNode = $valueDef->name;
- $valueName = $valueNameNode->value;
-
- if ($existingType instanceof EnumType && $existingType->getValue($valueName) !== null) {
- $context->reportError(new Error(
- "Enum value \"{$typeName}.{$valueName}\" already exists in the schema. It cannot also be defined in this type extension.",
- $valueNameNode
- ));
- } elseif (isset($valueNames[$valueName])) {
- $context->reportError(new Error(
- "Enum value \"{$typeName}.{$valueName}\" can only be defined once.",
- [$valueNames[$valueName], $valueNameNode]
- ));
- } else {
- $valueNames[$valueName] = $valueNameNode;
- }
- }
-
- return Visitor::skipNode();
- };
-
- return [
- NodeKind::ENUM_TYPE_DEFINITION => $checkValueUniqueness,
- NodeKind::ENUM_TYPE_EXTENSION => $checkValueUniqueness,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php
deleted file mode 100644
index 66d7ce65751..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Unique field definition names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL complex type is only valid if all its fields are uniquely named.
- */
-class UniqueFieldDefinitionNames extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $schema = $context->getSchema();
-
- /** @var array<string, array<int, NameNode>> $knownFieldNames */
- $knownFieldNames = [];
-
- $checkFieldUniqueness = static function ($node) use ($context, $schema, &$knownFieldNames): VisitorOperation {
- assert(
- $node instanceof InputObjectTypeDefinitionNode
- || $node instanceof InputObjectTypeExtensionNode
- || $node instanceof InterfaceTypeDefinitionNode
- || $node instanceof InterfaceTypeExtensionNode
- || $node instanceof ObjectTypeDefinitionNode
- || $node instanceof ObjectTypeExtensionNode
- );
-
- $typeName = $node->name->value;
-
- $knownFieldNames[$typeName] ??= [];
- $fieldNames = &$knownFieldNames[$typeName];
-
- foreach ($node->fields as $fieldDef) {
- $fieldName = $fieldDef->name->value;
-
- $existingType = $schema !== null
- ? $schema->getType($typeName)
- : null;
- if (self::hasField($existingType, $fieldName)) {
- $context->reportError(
- new Error(
- "Field \"{$typeName}.{$fieldName}\" already exists in the schema. It cannot also be defined in this type extension.",
- $fieldDef->name,
- ),
- );
- } elseif (isset($fieldNames[$fieldName])) {
- $context->reportError(
- new Error(
- "Field \"{$typeName}.{$fieldName}\" can only be defined once.",
- [$fieldNames[$fieldName], $fieldDef->name],
- ),
- );
- } else {
- $fieldNames[$fieldName] = $fieldDef->name;
- }
- }
-
- return Visitor::skipNode();
- };
-
- return [
- NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkFieldUniqueness,
- NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkFieldUniqueness,
- NodeKind::INTERFACE_TYPE_DEFINITION => $checkFieldUniqueness,
- NodeKind::INTERFACE_TYPE_EXTENSION => $checkFieldUniqueness,
- NodeKind::OBJECT_TYPE_DEFINITION => $checkFieldUniqueness,
- NodeKind::OBJECT_TYPE_EXTENSION => $checkFieldUniqueness,
- ];
- }
-
- /** @throws InvariantViolation */
- private static function hasField(?NamedType $type, string $fieldName): bool
- {
- if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
- return $type->hasField($fieldName);
- }
-
- return false;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php
deleted file mode 100644
index 62c0e788f41..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class UniqueFragmentNames extends ValidationRule
-{
- /** @var array<string, NameNode> */
- protected array $knownFragmentNames;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->knownFragmentNames = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
- NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation {
- $fragmentName = $node->name->value;
- if (! isset($this->knownFragmentNames[$fragmentName])) {
- $this->knownFragmentNames[$fragmentName] = $node->name;
- } else {
- $context->reportError(new Error(
- static::duplicateFragmentNameMessage($fragmentName),
- [$this->knownFragmentNames[$fragmentName], $node->name]
- ));
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-
- public static function duplicateFragmentNameMessage(string $fragName): string
- {
- return "There can be only one fragment named \"{$fragName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php
deleted file mode 100644
index 9bea56dad25..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
-
-/**
- * @phpstan-import-type VisitorArray from Visitor
- */
-class UniqueInputFieldNames extends ValidationRule
-{
- /** @var array<string, NameNode> */
- protected array $knownNames;
-
- /** @var array<array<string, NameNode>> */
- protected array $knownNameStack;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return $this->getASTVisitor($context);
- }
-
- /** @phpstan-return VisitorArray */
- public function getASTVisitor(ValidationContext $context): array
- {
- $this->knownNames = [];
- $this->knownNameStack = [];
-
- return [
- NodeKind::OBJECT => [
- 'enter' => function (): void {
- $this->knownNameStack[] = $this->knownNames;
- $this->knownNames = [];
- },
- 'leave' => function (): void {
- $knownNames = array_pop($this->knownNameStack);
- assert(is_array($knownNames), 'should not happen if the visitor works correctly');
-
- $this->knownNames = $knownNames;
- },
- ],
- NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context): VisitorOperation {
- $fieldName = $node->name->value;
-
- if (isset($this->knownNames[$fieldName])) {
- $context->reportError(new Error(
- static::duplicateInputFieldMessage($fieldName),
- [$this->knownNames[$fieldName], $node->name]
- ));
- } else {
- $this->knownNames[$fieldName] = $node->name;
- }
-
- return Visitor::skipNode();
- },
- ];
- }
-
- public static function duplicateInputFieldMessage(string $fieldName): string
- {
- return "There can be only one input field named \"{$fieldName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php
deleted file mode 100644
index 482bc63f285..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class UniqueOperationNames extends ValidationRule
-{
- /** @var array<string, NameNode> */
- protected array $knownOperationNames;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->knownOperationNames = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context): VisitorOperation {
- $operationName = $node->name;
-
- if ($operationName !== null) {
- if (! isset($this->knownOperationNames[$operationName->value])) {
- $this->knownOperationNames[$operationName->value] = $operationName;
- } else {
- $context->reportError(new Error(
- static::duplicateOperationNameMessage($operationName->value),
- [$this->knownOperationNames[$operationName->value], $operationName]
- ));
- }
- }
-
- return Visitor::skipNode();
- },
- NodeKind::FRAGMENT_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
- ];
- }
-
- public static function duplicateOperationNameMessage(string $operationName): string
- {
- return "There can be only one operation named \"{$operationName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php
deleted file mode 100644
index 9bb92fb43b7..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Unique operation types.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if it has only one type per operation.
- */
-class UniqueOperationTypes extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $schema = $context->getSchema();
- $definedOperationTypes = [];
- $existingOperationTypes = $schema !== null
- ? [
- 'query' => $schema->getQueryType(),
- 'mutation' => $schema->getMutationType(),
- 'subscription' => $schema->getSubscriptionType(),
- ]
- : [];
-
- /**
- * @param SchemaDefinitionNode|SchemaExtensionNode $node
- */
- $checkOperationTypes = static function ($node) use ($context, &$definedOperationTypes, $existingOperationTypes): VisitorOperation {
- foreach ($node->operationTypes as $operationType) {
- $operation = $operationType->operation;
- $alreadyDefinedOperationType = $definedOperationTypes[$operation] ?? null;
-
- if (isset($existingOperationTypes[$operation])) {
- $context->reportError(
- new Error(
- "Type for {$operation} already defined in the schema. It cannot be redefined.",
- $operationType,
- ),
- );
- } elseif ($alreadyDefinedOperationType !== null) {
- $context->reportError(
- new Error(
- "There can be only one {$operation} type in schema.",
- [$alreadyDefinedOperationType, $operationType],
- ),
- );
- } else {
- $definedOperationTypes[$operation] = $operationType;
- }
- }
-
- return Visitor::skipNode();
- };
-
- return [
- NodeKind::SCHEMA_DEFINITION => $checkOperationTypes,
- NodeKind::SCHEMA_EXTENSION => $checkOperationTypes,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php
deleted file mode 100644
index a55d0e6d3f1..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * Unique type names.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all defined types have unique names.
- */
-class UniqueTypeNames extends ValidationRule
-{
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- $schema = $context->getSchema();
- /** @var array<string, NameNode> $knownTypeNames */
- $knownTypeNames = [];
- $checkTypeName = static function ($node) use ($context, $schema, &$knownTypeNames): ?VisitorOperation {
- $typeName = $node->name->value;
-
- if ($schema !== null && $schema->getType($typeName) !== null) {
- $context->reportError(
- new Error(
- "Type \"{$typeName}\" already exists in the schema. It cannot also be defined in this type definition.",
- $node->name,
- ),
- );
-
- return null;
- }
-
- if (array_key_exists($typeName, $knownTypeNames)) {
- $context->reportError(
- new Error(
- "There can be only one type named \"{$typeName}\".",
- [
- $knownTypeNames[$typeName],
- $node->name,
- ]
- ),
- );
- } else {
- $knownTypeNames[$typeName] = $node->name;
- }
-
- return Visitor::skipNode();
- };
-
- return [
- NodeKind::SCALAR_TYPE_DEFINITION => $checkTypeName,
- NodeKind::OBJECT_TYPE_DEFINITION => $checkTypeName,
- NodeKind::INTERFACE_TYPE_DEFINITION => $checkTypeName,
- NodeKind::UNION_TYPE_DEFINITION => $checkTypeName,
- NodeKind::ENUM_TYPE_DEFINITION => $checkTypeName,
- NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkTypeName,
- ];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php
deleted file mode 100644
index 510e6a4a4ff..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class UniqueVariableNames extends ValidationRule
-{
- /** @var array<string, NameNode> */
- protected array $knownVariableNames;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- $this->knownVariableNames = [];
-
- return [
- NodeKind::OPERATION_DEFINITION => function (): void {
- $this->knownVariableNames = [];
- },
- NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context): void {
- $variableName = $node->variable->name->value;
- if (! isset($this->knownVariableNames[$variableName])) {
- $this->knownVariableNames[$variableName] = $node->variable->name;
- } else {
- $context->reportError(new Error(
- static::duplicateVariableMessage($variableName),
- [$this->knownVariableNames[$variableName], $node->variable->name]
- ));
- }
- },
- ];
- }
-
- public static function duplicateVariableMessage(string $variableName): string
- {
- return "There can be only one variable named \"{$variableName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php
deleted file mode 100644
index 857d077251e..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
-
-/**
- * @phpstan-import-type VisitorArray from Visitor
- */
-abstract class ValidationRule
-{
- protected string $name;
-
- public function getName(): string
- {
- return $this->name ?? static::class;
- }
-
- /**
- * Returns structure suitable for @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor.
- *
- * @phpstan-return VisitorArray
- */
- public function getVisitor(QueryValidationContext $context): array
- {
- return [];
- }
-
- /**
- * Returns structure suitable for @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor.
- *
- * @phpstan-return VisitorArray
- */
- public function getSDLVisitor(SDLValidationContext $context): array
- {
- return [];
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php
deleted file mode 100644
index 7dd4db18faa..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php
+++ /dev/null
@@ -1,192 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-/**
- * Value literals of correct type.
- *
- * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all value literals are of the type
- * expected at their position.
- */
-class ValuesOfCorrectType extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::NULL => static function (NullValueNode $node) use ($context): void {
- $type = $context->getInputType();
- if ($type instanceof NonNull) {
- $typeStr = Utils::printSafe($type);
- $nodeStr = Printer::doPrint($node);
- $context->reportError(
- new Error(
- "Expected value of type \"{$typeStr}\", found {$nodeStr}.",
- $node
- )
- );
- }
- },
- NodeKind::LST => function (ListValueNode $node) use ($context): ?VisitorOperation {
- // Note: TypeInfo will traverse into a list's item type, so look to the
- // parent input type to check if it is a list.
- $parentType = $context->getParentInputType();
- $type = $parentType === null
- ? null
- : Type::getNullableType($parentType);
- if (! $type instanceof ListOfType) {
- $this->isValidValueNode($context, $node);
-
- return Visitor::skipNode();
- }
-
- return null;
- },
- NodeKind::OBJECT => function (ObjectValueNode $node) use ($context): ?VisitorOperation {
- $type = Type::getNamedType($context->getInputType());
- if (! $type instanceof InputObjectType) {
- $this->isValidValueNode($context, $node);
-
- return Visitor::skipNode();
- }
-
- // Ensure every required field exists.
- $inputFields = $type->getFields();
-
- $fieldNodeMap = [];
- foreach ($node->fields as $field) {
- $fieldNodeMap[$field->name->value] = $field;
- }
-
- foreach ($inputFields as $inputFieldName => $fieldDef) {
- if (! isset($fieldNodeMap[$inputFieldName]) && $fieldDef->isRequired()) {
- $fieldType = Utils::printSafe($fieldDef->getType());
- $context->reportError(
- new Error(
- "Field {$type->name}.{$inputFieldName} of required type {$fieldType} was not provided.",
- $node
- )
- );
- }
- }
-
- return null;
- },
- NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context): void {
- $parentType = Type::getNamedType($context->getParentInputType());
- if (! $parentType instanceof InputObjectType) {
- return;
- }
-
- if ($context->getInputType() !== null) {
- return;
- }
-
- $suggestions = Utils::suggestionList(
- $node->name->value,
- array_keys($parentType->getFields())
- );
- $didYouMean = $suggestions === []
- ? null
- : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?';
-
- $context->reportError(
- new Error(
- "Field \"{$node->name->value}\" is not defined by type \"{$parentType->name}\".{$didYouMean}",
- $node
- )
- );
- },
- NodeKind::ENUM => function (EnumValueNode $node) use ($context): void {
- $this->isValidValueNode($context, $node);
- },
- NodeKind::INT => function (IntValueNode $node) use ($context): void {
- $this->isValidValueNode($context, $node);
- },
- NodeKind::FLOAT => function (FloatValueNode $node) use ($context): void {
- $this->isValidValueNode($context, $node);
- },
- NodeKind::STRING => function (StringValueNode $node) use ($context): void {
- $this->isValidValueNode($context, $node);
- },
- NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context): void {
- $this->isValidValueNode($context, $node);
- },
- ];
- }
-
- /**
- * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node
- *
- * @throws \JsonException
- */
- protected function isValidValueNode(QueryValidationContext $context, ValueNode $node): void
- {
- // Report any error at the full type expected by the location.
- $locationType = $context->getInputType();
- if ($locationType === null) {
- return;
- }
-
- $type = Type::getNamedType($locationType);
-
- if (! $type instanceof LeafType) {
- $typeStr = Utils::printSafe($type);
- $nodeStr = Printer::doPrint($node);
- $context->reportError(
- new Error(
- "Expected value of type \"{$typeStr}\", found {$nodeStr}.",
- $node
- )
- );
-
- return;
- }
-
- // Scalars determine if a literal value is valid via parseLiteral() which
- // may throw to indicate failure.
- try {
- $type->parseLiteral($node);
- } catch (\Throwable $error) {
- if ($error instanceof Error) {
- $context->reportError($error);
- } else {
- $typeStr = Utils::printSafe($type);
- $nodeStr = Printer::doPrint($node);
- $context->reportError(
- new Error(
- "Expected value of type \"{$typeStr}\", found {$nodeStr}; {$error->getMessage()}",
- $node,
- null,
- [],
- null,
- $error // Ensure a reference to the original error is maintained.
- )
- );
- }
- }
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php
deleted file mode 100644
index 29957dba420..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class VariablesAreInputTypes extends ValidationRule
-{
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context): void {
- $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->type);
-
- // If the variable type is not an input type, return an error.
- if ($type === null || Type::isInputType($type)) {
- return;
- }
-
- $variableName = $node->variable->name->value;
- $context->reportError(new Error(
- static::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
- [$node->type]
- ));
- },
- ];
- }
-
- public static function nonInputTypeOnVarMessage(string $variableName, string $typeName): string
- {
- return "Variable \"\${$variableName}\" cannot be non-input type \"{$typeName}\".";
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php
deleted file mode 100644
index 0f373e7f15a..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeComparators;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-
-class VariablesInAllowedPosition extends ValidationRule
-{
- /**
- * A map from variable names to their definition nodes.
- *
- * @var array<string, VariableDefinitionNode>
- */
- protected array $varDefMap;
-
- public function getVisitor(QueryValidationContext $context): array
- {
- return [
- NodeKind::OPERATION_DEFINITION => [
- 'enter' => function (): void {
- $this->varDefMap = [];
- },
- 'leave' => function (OperationDefinitionNode $operation) use ($context): void {
- $usages = $context->getRecursiveVariableUsages($operation);
-
- foreach ($usages as $usage) {
- $node = $usage['node'];
- $type = $usage['type'];
- $defaultValue = $usage['defaultValue'];
- $varName = $node->name->value;
- $varDef = $this->varDefMap[$varName] ?? null;
-
- if ($varDef === null || $type === null) {
- continue;
- }
-
- // A var type is allowed if it is the same or more strict (e.g. is
- // a subtype of) than the expected type. It can be more strict if
- // the variable type is non-null when the expected type is nullable.
- // If both are list types, the variable item type can be more strict
- // than the expected item type (contravariant).
- $schema = $context->getSchema();
- $varType = AST::typeFromAST([$schema, 'getType'], $varDef->type);
-
- if ($varType !== null && ! $this->allowedVariableUsage($schema, $varType, $varDef->defaultValue, $type, $defaultValue)) {
- $context->reportError(new Error(
- static::badVarPosMessage($varName, $varType->toString(), $type->toString()),
- [$varDef, $node]
- ));
- }
- }
- },
- ],
- NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode): void {
- $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
- },
- ];
- }
-
- /**
- * A var type is allowed if it is the same or more strict than the expected
- * type. It can be more strict if the variable type is non-null when the
- * expected type is nullable. If both are list types, the variable item type can
- * be more strict than the expected item type.
- */
- public static function badVarPosMessage(string $varName, string $varType, string $expectedType): string
- {
- return "Variable \"\${$varName}\" of type \"{$varType}\" used in position expecting type \"{$expectedType}\".";
- }
-
- /**
- * Returns true if the variable is allowed in the location it was found,
- * which includes considering if default values exist for either the variable
- * or the location at which it is located.
- *
- * @param ValueNode|null $varDefaultValue
- * @param mixed $locationDefaultValue
- *
- * @throws InvariantViolation
- */
- protected function allowedVariableUsage(Schema $schema, Type $varType, $varDefaultValue, Type $locationType, $locationDefaultValue): bool
- {
- if ($locationType instanceof NonNull && ! $varType instanceof NonNull) {
- $hasNonNullVariableDefaultValue = $varDefaultValue !== null && ! $varDefaultValue instanceof NullValueNode;
- $hasLocationDefaultValue = Utils::undefined() !== $locationDefaultValue;
- if (! $hasNonNullVariableDefaultValue && ! $hasLocationDefaultValue) {
- return false;
- }
-
- $nullableLocationType = $locationType->getWrappedType();
-
- return TypeComparators::isTypeSubTypeOf($schema, $varType, $nullableLocationType);
- }
-
- return TypeComparators::isTypeSubTypeOf($schema, $varType, $locationType);
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php
deleted file mode 100644
index f6227362ae6..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-class SDLValidationContext implements ValidationContext
-{
- protected DocumentNode $ast;
-
- protected ?Schema $schema;
-
- /** @var list<Error> */
- protected array $errors = [];
-
- public function __construct(DocumentNode $ast, ?Schema $schema)
- {
- $this->ast = $ast;
- $this->schema = $schema;
- }
-
- public function reportError(Error $error): void
- {
- $this->errors[] = $error;
- }
-
- public function getErrors(): array
- {
- return $this->errors;
- }
-
- public function getDocument(): DocumentNode
- {
- return $this->ast;
- }
-
- public function getSchema(): ?Schema
- {
- return $this->schema;
- }
-}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php
deleted file mode 100644
index f6a43b3a3da..00000000000
--- a/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-
-interface ValidationContext
-{
- public function reportError(Error $error): void;
-
- /** @return list<Error> */
- public function getErrors(): array;
-
- public function getDocument(): DocumentNode;
-
- public function getSchema(): ?Schema;
-}
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index abe502a32a7..27359d962ee 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -21,9 +21,6 @@
"build:project:classic-assets": "pnpm --filter='@woocommerce/classic-assets' build",
"build:project:packages": "XDEBUG_MODE=off composer install --quiet",
"build:project:actualize-translation-domains": "node ./bin/package-update-textdomain.js",
- "build:api": "php bin/api-builder/build-api.php",
- "build:api:check": "php bin/api-builder/check-api-staleness.php",
- "build:api:test": "php bin/api-builder/build-api.php --api-dir=tests/php/src/Internal/Api/Fixtures/DummyApi --autogen-dir=tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated --api-namespace='Automattic\\WooCommerce\\Tests\\Internal\\Api\\Fixtures\\DummyApi' --autogen-namespace='Automattic\\WooCommerce\\Tests\\Internal\\Api\\Fixtures\\DummyApiAutogenerated'",
"changelog": "XDEBUG_MODE=off composer install --quiet && composer exec -- changelogger",
"update:php": "XDEBUG_MODE=off composer update --quiet",
"env:destroy": "pnpm env:dev:destroy",
@@ -122,9 +119,6 @@
"wp-env:test": "wp-env --config .wp-env.test.json"
},
"lint-staged": {
- "src/Api/**/*.php": [
- "php src/Internal/Api/DesignTime/Scripts/check-api-staleness.php"
- ],
"*.php": [
"php -d display_errors=1 -l",
"composer run-script lint-staged"
@@ -184,9 +178,7 @@
"name": "PHP: 7.4 WP: latest - 1",
"testType": "unit:php",
"command": "test:php:env",
- "shardingArguments": [
- "--testsuite=wc-phpunit-legacy,wc-phpunit-main"
- ],
+ "shardingArguments": [],
"onlyForDependencies": [],
"changes": [
"client/admin/config/*.json",
@@ -216,9 +208,7 @@
"optional": true,
"testType": "unit:php",
"command": "test:php:env",
- "shardingArguments": [
- "--testsuite=wc-phpunit-legacy,wc-phpunit-main"
- ],
+ "shardingArguments": [],
"onlyForDependencies": [],
"changes": [
"tests/legacy/**",
diff --git a/plugins/woocommerce/phpcs.xml b/plugins/woocommerce/phpcs.xml
index b451992e4a5..db630ac8883 100644
--- a/plugins/woocommerce/phpcs.xml
+++ b/plugins/woocommerce/phpcs.xml
@@ -19,8 +19,6 @@
<exclude-pattern>php-stubs/</exclude-pattern>
<!-- Custom PHPCS sniffs: must follow the PHP_CodeSniffer API naming, not WordPress'. -->
<exclude-pattern>bin/phpcs/</exclude-pattern>
- <!-- GraphQL test fixtures used only by unit tests. -->
- <exclude-pattern>tests/php/src/Internal/Api/Fixtures/</exclude-pattern>
<!-- Fixture PHP files used in e2e tests. -->
<exclude-pattern>tests/e2e/themes/blocks/</exclude-pattern>
<exclude-pattern>tests/e2e/test-plugins/blocks/</exclude-pattern>
@@ -80,51 +78,6 @@
<rule ref="PHPCompatibility">
<exclude-pattern>tests/</exclude-pattern>
- <exclude-pattern>src/Api/</exclude-pattern>
- <exclude-pattern>src/Internal/Api/</exclude-pattern>
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
-
- <!-- The Code API and its infrastructure require PHP 8.1+. CI runs on PHP 7.4,
- so Generic.PHP.Syntax (which shells out to `php -l`) flags every enum,
- constructor promotion, named argument, and union type as a parse error. -->
- <rule ref="Generic.PHP.Syntax">
- <exclude-pattern>src/Api/</exclude-pattern>
- <exclude-pattern>src/Internal/Api/</exclude-pattern>
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
-
- <!-- PHP 8.0 `mixed` type hint is valid but not recognized by the Squiz sniff -->
- <rule ref="Squiz.Commenting.FunctionComment.InvalidTypeHint">
- <exclude-pattern>src/Api/</exclude-pattern>
- </rule>
-
- <!-- tax_query / meta_query are intentional for product filtering -->
- <rule ref="WordPress.DB.SlowDBQuery">
- <exclude-pattern>src/Api/</exclude-pattern>
- </rule>
-
- <!-- API public classes: suppress variable comments where #[Description] attributes serve as documentation -->
- <rule ref="Squiz.Commenting.VariableComment.Missing">
- <exclude-pattern>src/Api/Types/</exclude-pattern>
- <exclude-pattern>src/Api/InputTypes/</exclude-pattern>
- <exclude-pattern>src/Api/Pagination/</exclude-pattern>
- </rule>
-
- <!-- Cursor-based pagination legitimately uses base64 for opaque cursors -->
- <rule ref="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode">
- <exclude-pattern>src/Api/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode">
- <exclude-pattern>src/Api/</exclude-pattern>
- </rule>
-
- <!-- Reserved keyword parameter names ($type, $default, $class, $parent) are
- unavoidable in attribute definitions, autoloader closures, and generated code. -->
- <rule ref="Universal.NamingConventions.NoReservedKeywordParameterNames">
- <exclude-pattern>src/Api/Attributes/Parameter.php</exclude-pattern>
- <exclude-pattern>src/Api/Infrastructure/</exclude-pattern>
- <exclude-pattern>src/Internal/Api/</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Files.TypeNameMatchesFileName">
@@ -189,7 +142,6 @@
<exclude-pattern>tests/</exclude-pattern>
<exclude-pattern>src/</exclude-pattern>
<exclude-pattern>tests/php/src/</exclude-pattern>
- <exclude-pattern>bin/api-builder/</exclude-pattern>
</rule>
<rule ref="Squiz.Classes.ClassFileName">
@@ -227,7 +179,6 @@
<exclude-pattern>src/</exclude-pattern>
<exclude-pattern>tests/php</exclude-pattern>
<exclude-pattern>tests/Tools/</exclude-pattern>
- <exclude-pattern>bin/api-builder/</exclude-pattern>
</rule>
<rule ref="Squiz.Commenting.FileComment.MissingPackageTag">
@@ -255,113 +206,6 @@
<severity>0</severity>
</rule>
- <!-- Autogenerated API code: suppress rules for generated files -->
- <rule ref="Generic.Commenting">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
- <rule ref="Squiz.Commenting">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
- <rule ref="WordPress.Security.EscapeOutput">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
- <rule ref="WordPress.WP.AlternativeFunctions">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
- <rule ref="Generic.CodeAnalysis.UnusedFunctionParameter">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
- <rule ref="WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- <exclude-pattern>src/Api/Infrastructure/GraphQLControllerBase.php</exclude-pattern>
- <exclude-pattern>src/Api/Infrastructure/QueryInfoExtractor.php</exclude-pattern>
- <exclude-pattern>src/Internal/Api/QueryComplexityRule.php</exclude-pattern>
- <exclude-pattern>src/Internal/Api/QueryDepthRule.php</exclude-pattern>
- </rule>
-
- <!-- API build scripts use empty if/elseif for intentional no-ops (e.g. enum
- properties need no conversion) with a comment explaining why. -->
- <rule ref="Generic.CodeAnalysis.EmptyStatement">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
-
- <!-- Autogenerated code: suppress additional rules -->
- <rule ref="Generic.PHP.Syntax">
- <exclude-pattern>src/Internal/Api/Autogenerated/</exclude-pattern>
- </rule>
-
- <!-- API templates: suppress rules that don't apply to PHP templates -->
- <rule ref="Generic.PHP.RequireStrictTypes">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="PSR12.Files.FileHeader">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="Generic.Commenting">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="Squiz.Commenting">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DiscouragedPHPFunctions.serialize_var_export">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.CodeAnalysis.AssignmentInTernaryCondition">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DontExtract">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.Security.EscapeOutput">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.YodaConditions">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DevelopmentFunctions">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="Generic.WhiteSpace.ScopeIndent">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
- <rule ref="WordPress.WP.GlobalVariablesOverride">
- <exclude-pattern>bin/api-builder/code-templates/</exclude-pattern>
- </rule>
-
- <!-- API build scripts: suppress WordPress-specific rules (CLI-only code) -->
- <rule ref="WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="WordPress.WP.AlternativeFunctions">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- <exclude-pattern>tests/php/src/Api/Infrastructure/DesignTime/</exclude-pattern>
- </rule>
- <rule ref="WordPress.Security.EscapeOutput">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.YodaConditions">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DontExtract">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="WordPress.PHP.DevelopmentFunctions">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="Generic.Commenting">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="Squiz.Commenting">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- <exclude-pattern>tests/php/src/Api/Infrastructure/DesignTime/</exclude-pattern>
- </rule>
- <rule ref="Universal.NamingConventions.NoReservedKeywordParameterNames">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
- <rule ref="WooCommerce.Functions.InternalInjectionMethod">
- <exclude-pattern>bin/api-builder/</exclude-pattern>
- </rule>
-
<!-- Temporary -->
<rule ref="Universal.Arrays.DisallowShortArraySyntax.Found">
<exclude-pattern>src/Blocks/</exclude-pattern>
diff --git a/plugins/woocommerce/phpstan.neon b/plugins/woocommerce/phpstan.neon
index de38b3df5a0..f69c412bfcc 100644
--- a/plugins/woocommerce/phpstan.neon
+++ b/plugins/woocommerce/phpstan.neon
@@ -14,14 +14,6 @@ parameters:
# Matches the prior test implementation; GeoIP relies on data files.
- includes/class-wc-geo-ip.php
- includes/react-admin/feature-config.php (?)
- # The Code API (src/Api/) and its infrastructure (src/Internal/Api/)
- # require PHP 8.1+ and use enums, named arguments, constructor
- # property promotion, union types, and `mixed` throughout — all of
- # which the global phpVersion: 70400 setting cannot parse or resolve.
- # Infrastructure files also reference src/Api/ classes that PHPStan
- # can't discover once the public API dir is excluded.
- - src/Api/
- - src/Internal/Api/
bootstrapFiles:
- vendor/autoload.php
scanDirectories:
diff --git a/plugins/woocommerce/phpunit.xml b/plugins/woocommerce/phpunit.xml
index 916f7ff7898..53b5fdf5e99 100644
--- a/plugins/woocommerce/phpunit.xml
+++ b/plugins/woocommerce/phpunit.xml
@@ -7,7 +7,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
verbose="true"
- defaultTestSuite="wc-phpunit-legacy,wc-phpunit-main,wc-phpunit-graphql"
+ defaultTestSuite="wc-phpunit-legacy,wc-phpunit-main"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<testsuites>
<testsuite name="wc-phpunit-legacy">
@@ -20,25 +20,6 @@
<!-- Email-template fixtures are plain PHP that emit markup at
include-time (top-level HTML and `echo`). Exclude to silence them. -->
<exclude>./tests/php/src/Internal/EmailEditor/WCTransactionalEmails/fixtures</exclude>
- <!-- Enumerated rather than a blanket ./tests/php/src/Api, so a new
- ./tests/php/src/Api/<X>/ subdir is never silently picked up by
- this suite. A new GraphQL test subdir must be added to the
- wc-phpunit-graphql testsuite AND excluded here. -->
- <exclude>./tests/php/src/Api/Infrastructure</exclude>
- <exclude>./tests/php/src/Api/Queries</exclude>
- <exclude>./tests/php/src/Api/Mutations</exclude>
- <exclude>./tests/php/src/Internal/Api</exclude>
- </testsuite>
- <!-- GraphQL tests: the manually-maintained infrastructure under
- src/Internal/Api/ and src/Api/Infrastructure/ (plus the resolver
- tree generated by ApiBuilder against the dummy fixture API), and
- the public command classes under src/Api/Queries/ and
- src/Api/Mutations/ (products, coupons, …). -->
- <testsuite name="wc-phpunit-graphql">
- <directory suffix=".php">./tests/php/src/Api/Infrastructure</directory>
- <directory suffix=".php">./tests/php/src/Internal/Api</directory>
- <directory suffix=".php">./tests/php/src/Api/Queries</directory>
- <directory suffix=".php">./tests/php/src/Api/Mutations</directory>
</testsuite>
</testsuites>
<listeners>
diff --git a/plugins/woocommerce/src/Api/ApiException.php b/plugins/woocommerce/src/Api/ApiException.php
deleted file mode 100644
index 1b0d58816fd..00000000000
--- a/plugins/woocommerce/src/Api/ApiException.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Exception for API errors with error codes and extensions.
- */
-class ApiException extends \RuntimeException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param string $error_code The machine-readable error code.
- * @param array $extensions Additional error metadata.
- * @param int $status_code The HTTP status code.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message,
- private readonly string $error_code = 'INTERNAL_ERROR',
- private readonly array $extensions = array(),
- int $status_code = 500,
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, $status_code, $previous );
- }
-
- /**
- * Get the machine-readable error code.
- *
- * @return string
- */
- public function getErrorCode(): string {
- return $this->error_code;
- }
-
- /**
- * Get the additional error metadata.
- *
- * @return array
- */
- public function getExtensions(): array {
- return $this->extensions;
- }
-
- /**
- * Get the HTTP status code.
- *
- * @return int
- */
- public function getStatusCode(): int {
- return $this->getCode();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/ArrayOf.php b/plugins/woocommerce/src/Api/Attributes/ArrayOf.php
deleted file mode 100644
index dd58ea81058..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/ArrayOf.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Declares the element type for an array-typed property or return value.
- *
- * PHP arrays are untyped, so the builder cannot infer the element type via
- * reflection. Apply this attribute to tell the builder what GraphQL list type
- * to generate (e.g. `[Int!]`, `[String!]`).
- *
- * Example: `#[ArrayOf('int')]` on a `array $product_ids` property produces
- * the GraphQL type `[Int!]!`.
- */
-#[Attribute]
-final class ArrayOf {
- /**
- * Constructor.
- *
- * @param string $type A scalar name ('int', 'string', 'float', 'bool') or
- * a fully-qualified class name for output/enum types.
- */
- public function __construct(
- public readonly string $type,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/ConnectionOf.php b/plugins/woocommerce/src/Api/Attributes/ConnectionOf.php
deleted file mode 100644
index ae37a02ed95..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/ConnectionOf.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marks a query's return type as a Relay-style connection of the given node type.
- *
- * Applied to the `execute()` method of a query class that returns a `Connection`.
- * The builder uses this to generate the corresponding connection and edge GraphQL
- * types (e.g. `CouponConnection`, `CouponEdge`) and to wire the correct return
- * type in the schema.
- */
-#[Attribute]
-final class ConnectionOf {
- /**
- * Constructor.
- *
- * @param string $type The fully-qualified class name of the node type
- * (e.g. `Coupon::class`).
- */
- public function __construct(
- public readonly string $type,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Deprecated.php b/plugins/woocommerce/src/Api/Attributes/Deprecated.php
deleted file mode 100644
index 5cf48e0af38..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Deprecated.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marks a field or enum value as deprecated in the GraphQL schema.
- *
- * Deprecated elements remain functional but are flagged with a deprecation
- * reason in introspection, signaling to API consumers that they should
- * migrate to an alternative.
- */
-#[Attribute]
-final class Deprecated {
- /**
- * Constructor.
- *
- * @param string $reason A human-readable explanation of why the element is
- * deprecated and what to use instead.
- */
- public function __construct(
- public readonly string $reason,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Description.php b/plugins/woocommerce/src/Api/Attributes/Description.php
deleted file mode 100644
index 1cc20cf6672..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Description.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Provides a human-readable description for the annotated element.
- *
- * Can be applied to classes (types, queries, mutations, enums), properties, or
- * parameters. The text is exposed as the "description" field in the generated
- * GraphQL schema and is visible in tools like GraphiQL.
- */
-#[Attribute]
-final class Description {
- /**
- * Constructor.
- *
- * @param string $description The text to expose as the GraphQL description.
- */
- public function __construct(
- public readonly string $description,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Experimental.php b/plugins/woocommerce/src/Api/Attributes/Experimental.php
deleted file mode 100644
index 8e4f6af87be..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Experimental.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marks a code-API element as experimental: present in the schema, but not
- * stable enough to be relied on in production.
- *
- * Discoverable through the `_apiMetadata` GraphQL field as an entry with
- * `name = "experimental"` and `value = true`. The marking is informational:
- * it does not gate access in any way.
- *
- * When a class, property or enum case has this attribute, the generated
- * GraphQL `description` is prefixed with `[Experimental] `, and
- * when the element has no `#[Description]` at all, a default body
- * (`[Experimental] Not to be used in production environments.`) is emitted
- * so the marker still reaches stock introspection.
- *
- * `#[Experimental]` on a class marks only that class: its fields and enum
- * cases are not implicitly marked too. A tool that wants to treat the
- * contents of an experimental type as experimental by association must
- * apply that rule itself when it reads the metadata.
- *
- * The attribute targets classes, properties, and enum cases; see
- * {@see Metadata} for the reasoning behind excluding methods.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS_CONSTANT )]
-class Experimental extends Metadata {
- /**
- * Construct an `experimental` metadata entry with value `true`.
- */
- public function __construct() {
- parent::__construct( 'experimental', true );
- }
-
- /**
- * Prepend `[Experimental] ` to the description, supplying a default body
- * when the element has no `#[Description]` of its own. See
- * {@see Metadata::transform_description()} for the contract.
- *
- * @param string $description Incoming description (empty when no `#[Description]`).
- */
- public function transform_description( string $description ): string {
- if ( '' === $description ) {
- $description = 'Not to be used in production environments.';
- }
- return '[Experimental] ' . $description;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/HiddenFromMetadataQuery.php b/plugins/woocommerce/src/Api/Attributes/HiddenFromMetadataQuery.php
deleted file mode 100644
index d7491f6e608..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/HiddenFromMetadataQuery.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marker attribute that opts a code-API target out of the `_apiMetadata`
- * discovery query without affecting any other behaviour. Apply on a
- * class (output / input type, query, mutation) or on a property to hide
- * that target's row — and all its metadata / authorization descriptors —
- * from the `_apiMetadata` endpoint.
- *
- * This is unrelated to native GraphQL introspection (`__schema` /
- * `__type`); those queries continue to expose the schema's shape as
- * usual. The marker only affects the custom `_apiMetadata` channel.
- *
- * The runtime authorization gates emitted into the generated resolvers
- * are unaffected: an authorization attribute placed alongside this one
- * still runs its `authorize()` method; this marker just removes the
- * declarative shape from the discovery channel.
- *
- * A target's `_apiMetadata` visibility is the AND of every attribute's
- * `shows_in_metadata_query()` on the target — so combining
- * `#[HiddenFromMetadataQuery]` with any other attribute that returns
- * `true` (or none at all) still hides the target.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::TARGET_CLASS_CONSTANT )]
-final class HiddenFromMetadataQuery {
- /**
- * Always returns `false`. ApiBuilder calls this during the per-target
- * `_apiMetadata` visibility check; the target is omitted from the
- * discovery output as a result.
- */
- public function shows_in_metadata_query(): bool {
- return false;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Ignore.php b/plugins/woocommerce/src/Api/Attributes/Ignore.php
deleted file mode 100644
index df09b650e6a..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Ignore.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Tells the builder to skip the annotated element entirely.
- *
- * Apply to a class to exclude it from API discovery (e.g. helper classes that
- * live in a scanned namespace but are not part of the API), or to a property
- * to omit it from the generated GraphQL type.
- */
-#[Attribute]
-final class Ignore {
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Internal.php b/plugins/woocommerce/src/Api/Attributes/Internal.php
deleted file mode 100644
index 74cf0e3c2aa..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Internal.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marks a code-API element as for WooCommerce internal use.
- *
- * Discoverable through the `_apiMetadata` GraphQL field as an entry with
- * `name = "internal"` and `value = true`. The marking is informational —
- * authorization remains the job of {@see PublicAccess}, {@see RequiredCapability},
- * and any plugin-supplied authorization attributes.
- *
- * When a class, property or enum case has this attribute, the generated
- * GraphQL `description` is prefixed with `[Internal] `, and
- * when the element has no `#[Description]` at all, a default body
- * (`[Internal] For WooCommerce core internal usage only.`) is emitted so the
- * marker still reaches stock introspection.
- *
- * `#[Internal]` on a class marks only that class — its fields and enum cases
- * are not implicitly marked too. A tool that wants to treat the contents of
- * an internal type as internal by association must apply that rule itself
- * when it reads the metadata.
- *
- * The attribute targets classes, properties, and enum cases; see
- * {@see Metadata} for the reasoning behind excluding methods.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS_CONSTANT )]
-class Internal extends Metadata {
- /**
- * Construct an `internal` metadata entry with value `true`.
- */
- public function __construct() {
- parent::__construct( 'internal', true );
- }
-
- /**
- * Prepend `[Internal] ` to the description, supplying a default body when
- * the element has no `#[Description]` of its own. See
- * {@see Metadata::transform_description()} for the contract.
- *
- * @param string $description Incoming description (empty when no `#[Description]`).
- */
- public function transform_description( string $description ): string {
- if ( '' === $description ) {
- $description = 'For WooCommerce core internal usage only.';
- }
- return '[Internal] ' . $description;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Metadata.php b/plugins/woocommerce/src/Api/Attributes/Metadata.php
deleted file mode 100644
index 39a548137a1..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Metadata.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Attaches a name/value metadata entry to a code-API element.
- * (class, class property, method parameter, method parameter, or enum case).
- *
- * Metadata entries are harvested by ApiBuilder and emitted into the generated
- * schema, where they can be queried at runtime through the top-level
- * `_apiMetadata` GraphQL field. The mechanism is intentionally open: subclass
- * this attribute to ship a category of metadata (e.g. {@see Internal} for
- * marking elements as for WooCommerce internal use). Tooling discovers metadata
- * by name; the value is scalar-only so it can flow through GraphQL without
- * additional encoding.
- *
- * Two metadata entries with the same name on the same element produce a
- * build-time error, see ApiBuilder for the duplicate-name detection. Multiple
- * distinct names on one element are allowed.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::TARGET_CLASS_CONSTANT | Attribute::IS_REPEATABLE )]
-class Metadata {
- /**
- * Constructor.
- *
- * @param string $name Identifier for this entry. Must be unique per element across all Metadata subclasses applied to it.
- * @param bool|int|float|string|null $value Scalar payload exposed to clients via `_apiMetadata`.
- */
- public function __construct(
- private string $name,
- private bool|int|float|string|null $value,
- ) {
- }
-
- /**
- * The entry's name (e.g. `internal`, `beta`, `owner`).
- */
- public function get_name(): string {
- return $this->name;
- }
-
- /**
- * The entry's scalar value.
- */
- public function get_value(): bool|int|float|string|null {
- return $this->value;
- }
-
- /**
- * Whether the element carrying this attribute should appear in the
- * `_apiMetadata` discovery query.
- *
- * Returning `false` removes the element's row entirely from
- * `_apiMetadata` — neither this metadata entry nor any other
- * descriptor on the same target surfaces. The runtime gates and any
- * description transforms are unaffected. Useful for plugins that
- * attach internal routing or feature hints they prefer not to
- * broadcast through the discovery channel.
- *
- * Despite the colloquial naming around it, this has nothing to do
- * with native GraphQL introspection (`__schema` / `__type`); those
- * queries continue to expose the schema's shape as usual.
- *
- * Because this is an instance method, subclasses can decide
- * conditionally based on their own constructor arguments.
- */
- public function shows_in_metadata_query(): bool {
- return true;
- }
-
- /**
- * Transform the GraphQL `description` of the element this attribute is
- * applied to.
- *
- * The base implementation is a no-op; the general `#[Metadata]` mechanism
- * does not modify descriptions. Subclasses opt into the description-mirror
- * convention by overriding this method — typically to prefix the input
- * with a marker (`[Internal] `, `[Experimental] `, …) and supply a default
- * body when the element has no `#[Description]` of its own.
- *
- * Conventions for overrides:
- * - An empty `$description` means the element has no `#[Description]`.
- * If the subclass wants the marker to still reach stock introspection,
- * it should supply a sensible default text and prefix it as usual.
- * - A non-empty `$description` may be either the developer's own text or
- * the output of a previous attribute's transform; the subclass should
- * not try to distinguish. Wrap-only (prefix the input, don't replace).
- *
- * When more than one transforming attribute is applied to the same
- * element, ApiBuilder calls `transform_description()` once per attribute
- * in PHP reflection (source) order, threading each return value into the
- * next call. Because each subclass prefixes the input, the last attribute
- * in source ends up as the outermost prefix in the final string. Order
- * the attributes accordingly when the reading order matters.
- *
- * @internal Called by ApiBuilder; not part of any caller-visible contract.
- *
- * @param string $description The current description text (`''` when the element has no `#[Description]`, or the previous transform's output when chained).
- */
- public function transform_description( string $description ): string {
- return $description;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Name.php b/plugins/woocommerce/src/Api/Attributes/Name.php
deleted file mode 100644
index a0c5f3d325b..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Name.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Overrides the GraphQL name derived from the PHP class or property name.
- *
- * By default the builder converts PHP names to GraphQL conventions automatically.
- * Use this attribute when you need a specific GraphQL name that differs from
- * the default conversion (e.g. a legacy name for backwards compatibility).
- */
-#[Attribute]
-final class Name {
- /**
- * Constructor.
- *
- * @param string $name The exact name to use in the GraphQL schema.
- */
- public function __construct(
- public readonly string $name,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Parameter.php b/plugins/woocommerce/src/Api/Attributes/Parameter.php
deleted file mode 100644
index c6d7ec15c5c..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Parameter.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Declares an explicit GraphQL argument for a query or mutation.
- *
- * Use this when the argument cannot be inferred from the `execute()` method
- * signature — for example, when a parameter needs a specific GraphQL type,
- * nullability, or default that differs from what reflection would produce.
- * This attribute is repeatable: apply it once per argument.
- */
-#[Attribute( Attribute::TARGET_ALL | Attribute::IS_REPEATABLE )]
-final class Parameter {
- /**
- * Whether a default value was provided.
- *
- * @var bool
- */
- public readonly bool $has_default;
-
- /**
- * Constructor.
- *
- * @param string $name The GraphQL argument name (not needed when unrolling).
- * @param string $type The PHP type name ('int', 'string', 'float', 'bool')
- * or a fully-qualified class name for complex types.
- * @param bool $nullable Whether the argument accepts null.
- * @param bool $array Whether the argument is a list (e.g. `[Int!]`).
- * @param mixed $default The default value if the argument is omitted.
- * @param string $description Human-readable description for the schema.
- * @param bool $has_default Set to true to explicitly indicate a default is
- * provided (needed when the default value is null).
- * @param bool $unroll When true, the class given in $type is expanded into
- * individual GraphQL arguments (one per public property).
- */
- public function __construct(
- public readonly string $name = '',
- public readonly string $type = '',
- public readonly bool $nullable = false,
- public readonly bool $array = false,
- public readonly mixed $default = null,
- public readonly string $description = '',
- bool $has_default = false,
- public readonly bool $unroll = false,
- ) {
- // We need a separate flag because null could be a valid default value.
- // Callers pass has_default: true when they supply a default, or we infer
- // it from the default value being non-null.
- $this->has_default = $has_default || null !== $default;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/ParameterDescription.php b/plugins/woocommerce/src/Api/Attributes/ParameterDescription.php
deleted file mode 100644
index 0ee3816d420..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/ParameterDescription.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Adds a description to a query/mutation argument without overriding its type.
- *
- * Sets the description for a query/mutation argument. Can be used both for
- * arguments inferred from the `execute()` method signature and for arguments
- * declared via #[Parameter]. However, a parameter must not have a description
- * in both #[Parameter] and #[ParameterDescription] — that is a build error.
- * This attribute is repeatable: apply it once per argument that needs a
- * description.
- */
-#[Attribute( Attribute::TARGET_ALL | Attribute::IS_REPEATABLE )]
-final class ParameterDescription {
- /**
- * Constructor.
- *
- * @param string $name The argument name (must match the `execute()`
- * parameter name).
- * @param string $description Human-readable description for the schema.
- */
- public function __construct(
- public readonly string $name,
- public readonly string $description,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/PublicAccess.php b/plugins/woocommerce/src/Api/Attributes/PublicAccess.php
deleted file mode 100644
index 7c59267934f..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/PublicAccess.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Marks a query or mutation as publicly accessible without authentication.
- *
- * Mutually exclusive with #[RequiredCapability] (and any other authorization
- * attribute) on the same class — this is a hard build error.
- *
- * Placement on a property (output field or input field) is accepted but is
- * a build warning and a runtime no-op: it always grants, which is
- * indistinguishable from the default allow-by-default semantics for fields
- * that carry no authorization attribute.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY )]
-final class PublicAccess {
- /**
- * Always grants access.
- */
- public function authorize(): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/RequiredCapability.php b/plugins/woocommerce/src/Api/Attributes/RequiredCapability.php
deleted file mode 100644
index 5668bbbc8c8..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/RequiredCapability.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-
-/**
- * Declares a WordPress capability required to execute a query or mutation,
- * or to read an output field, or to set an input field on a mutation.
- *
- * This attribute is repeatable: apply it multiple times to require several
- * capabilities (logical AND).
- *
- * Mutually exclusive with #[PublicAccess] at the class level. At the field
- * level a `#[PublicAccess]` placement on the same property is a build
- * warning and is treated as a no-op.
- *
- * Targets: class (query/mutation/output type) and property (output field,
- * input field, or trait-declared property). Trait-declared properties
- * carry the attribute onto every implementing class through PHP's
- * reflection.
- */
-#[Attribute( Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY )]
-final class RequiredCapability {
- /**
- * Constructor.
- *
- * @param string $capability A WordPress capability slug
- * (e.g. 'manage_woocommerce').
- */
- public function __construct(
- public readonly string $capability,
- ) {
- }
-
- /**
- * Decide whether the given principal holds the required capability.
- *
- * Reads the WordPress user from the principal wrapper and delegates to
- * {@see \user_can()}. Anonymous principals (the WP user has `ID === 0`)
- * never hold any capability, so the check returns false naturally.
- *
- * @param Principal $principal The resolved request principal.
- */
- public function authorize( Principal $principal ): bool {
- return user_can( $principal->user, $this->capability );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/ReturnType.php b/plugins/woocommerce/src/Api/Attributes/ReturnType.php
deleted file mode 100644
index e2eae114d37..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/ReturnType.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Declares the GraphQL return type of execute() when it returns an interface.
- *
- * Since PHP cannot type-hint a trait, the execute() method uses `object` as its
- * return type and this attribute tells the builder which interface type to use
- * in the schema. The GraphQL engine then uses the interface's `resolveType`
- * callback to determine the concrete type at runtime.
- */
-#[Attribute( Attribute::TARGET_METHOD )]
-final class ReturnType {
- /**
- * Constructor.
- *
- * @param string $type The fully-qualified class name of the interface trait
- * (e.g. `ApiObject::class`).
- */
- public function __construct(
- public readonly string $type,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/ScalarType.php b/plugins/woocommerce/src/Api/Attributes/ScalarType.php
deleted file mode 100644
index 074b759179e..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/ScalarType.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Overrides the GraphQL type for a property with a custom scalar.
- *
- * By default the builder maps PHP types to built-in GraphQL scalars (String,
- * Int, Float, Boolean). Use this attribute when a property should use a custom
- * scalar type instead, such as `DateTime`.
- *
- * Example: `#[ScalarType(DateTime::class)]` on a `?string $date_created`
- * property produces the GraphQL type `DateTime` instead of `String`.
- */
-#[Attribute]
-final class ScalarType {
- /**
- * Constructor.
- *
- * @param string $type The fully-qualified class name of the custom scalar
- * (e.g. `DateTime::class`).
- */
- public function __construct(
- public readonly string $type,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/Attributes/Unroll.php b/plugins/woocommerce/src/Api/Attributes/Unroll.php
deleted file mode 100644
index 478ee9b79a0..00000000000
--- a/plugins/woocommerce/src/Api/Attributes/Unroll.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Attributes;
-
-use Attribute;
-
-/**
- * Expands a class's properties into individual flat GraphQL arguments.
- *
- * When applied to a class, any `execute()` parameter of that type is
- * automatically unrolled. When applied to a specific `execute()` parameter,
- * only that usage is unrolled.
- *
- * Each public property of the target class becomes a separate GraphQL argument.
- * Properties marked with #[Ignore] are skipped, and #[Description] on
- * properties is forwarded to the generated argument descriptions.
- *
- * The generated resolver constructs the original class via its constructor,
- * passing the individual argument values as named parameters.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PARAMETER )]
-final class Unroll {
-}
diff --git a/plugins/woocommerce/src/Api/Enums/Coupons/CouponStatus.php b/plugins/woocommerce/src/Api/Enums/Coupons/CouponStatus.php
deleted file mode 100644
index 106cc6e2001..00000000000
--- a/plugins/woocommerce/src/Api/Enums/Coupons/CouponStatus.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Enums\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'The publication status of a coupon.' )]
-enum CouponStatus: string {
- #[Description( 'The coupon is published and active.' )]
- case Published = 'publish';
-
- #[Description( 'The coupon is a draft.' )]
- case Draft = 'draft';
-
- #[Description( 'The coupon is pending review.' )]
- case Pending = 'pending';
-
- #[Description( 'The coupon is privately published.' )]
- case Private = 'private';
-
- #[Description( 'The coupon is scheduled to be published in the future.' )]
- case Future = 'future';
-
- #[Description( 'The coupon is in the trash.' )]
- case Trash = 'trash';
-
- #[Description( 'The coupon status is not one of the standard WordPress values (e.g. added by a plugin). Inspect raw_status for the underlying value.' )]
- case Other = 'other';
-}
diff --git a/plugins/woocommerce/src/Api/Enums/Coupons/DiscountType.php b/plugins/woocommerce/src/Api/Enums/Coupons/DiscountType.php
deleted file mode 100644
index 6ac7e353b90..00000000000
--- a/plugins/woocommerce/src/Api/Enums/Coupons/DiscountType.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Enums\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'The type of discount for a coupon.' )]
-enum DiscountType: string {
- #[Description( 'A percentage discount.' )]
- case Percent = 'percent';
-
- #[Description( 'A fixed amount discount applied to the cart.' )]
- case FixedCart = 'fixed_cart';
-
- #[Description( 'A fixed amount discount applied to each eligible product.' )]
- case FixedProduct = 'fixed_product';
-
- #[Description( 'The discount type is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_discount_type for the underlying value.' )]
- case Other = 'other';
-}
diff --git a/plugins/woocommerce/src/Api/Enums/Products/ProductStatus.php b/plugins/woocommerce/src/Api/Enums/Products/ProductStatus.php
deleted file mode 100644
index 0c64e7bb2d0..00000000000
--- a/plugins/woocommerce/src/Api/Enums/Products/ProductStatus.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Enums\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Deprecated;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-
-#[Description( 'The publication status of a product.' )]
-enum ProductStatus: string {
- #[Description( 'The product is a draft.' )]
- case Draft = 'draft';
-
- #[Description( 'The product is pending review.' )]
- case Pending = 'pending';
-
- #[Name( 'ACTIVE' )]
- #[Description( 'The product is published and visible.' )]
- case Published = 'publish';
-
- #[Description( 'The product is privately published.' )]
- case Private = 'private';
-
- #[Description( 'The product is scheduled to be published in the future.' )]
- case Future = 'future';
-
- #[Deprecated( 'Trashed products should be excluded via status filter.' )]
- #[Description( 'The product is in the trash.' )]
- case Trash = 'trash';
-
- #[Description( 'The product status is not one of the standard WordPress values (e.g. added by a plugin). Inspect raw_status for the underlying value.' )]
- case Other = 'other';
-}
diff --git a/plugins/woocommerce/src/Api/Enums/Products/ProductType.php b/plugins/woocommerce/src/Api/Enums/Products/ProductType.php
deleted file mode 100644
index 32a94190596..00000000000
--- a/plugins/woocommerce/src/Api/Enums/Products/ProductType.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Enums\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'The type of a WooCommerce product.' )]
-enum ProductType: string {
- #[Description( 'A simple product.' )]
- case Simple = 'simple';
-
- #[Description( 'A grouped product.' )]
- case Grouped = 'grouped';
-
- #[Description( 'An external/affiliate product.' )]
- case External = 'external';
-
- #[Description( 'A variable product with variations.' )]
- case Variable = 'variable';
-
- #[Description( 'A product variation.' )]
- case Variation = 'variation';
-
- #[Description( 'The product type is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_product_type for the underlying value.' )]
- case Other = 'other';
-}
diff --git a/plugins/woocommerce/src/Api/Enums/Products/StockStatus.php b/plugins/woocommerce/src/Api/Enums/Products/StockStatus.php
deleted file mode 100644
index 8e1a6c3ebc7..00000000000
--- a/plugins/woocommerce/src/Api/Enums/Products/StockStatus.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Enums\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'The stock status of a product.' )]
-enum StockStatus: int {
- #[Description( 'The product is in stock.' )]
- case InStock = 1;
-
- #[Description( 'The product is out of stock.' )]
- case OutOfStock = 2;
-
- #[Description( 'The product is on backorder.' )]
- case OnBackorder = 3;
-
- #[Description( 'The stock status is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_stock_status for the underlying value.' )]
- case Other = 4;
-}
diff --git a/plugins/woocommerce/src/Api/ForbiddenException.php b/plugins/woocommerce/src/Api/ForbiddenException.php
deleted file mode 100644
index 9c3ea7b3fc1..00000000000
--- a/plugins/woocommerce/src/Api/ForbiddenException.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Thrown to signal that the caller is authenticated but lacks permission to
- * perform the requested operation, e.g. the right user but the wrong role,
- * scope, or capability.
- *
- * Use this for "I know who you are, but you can't do this." For "you need to
- * authenticate first," prefer {@see UnauthorizedException}.
- *
- * Wire shape: `extensions.code = 'FORBIDDEN'`, HTTP status 403.
- */
-class ForbiddenException extends ApiException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param array $extensions Additional error metadata to surface in the GraphQL `extensions` object.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message = 'Forbidden.',
- array $extensions = array(),
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, 'FORBIDDEN', $extensions, 403, $previous );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/ClassResolver.php b/plugins/woocommerce/src/Api/Infrastructure/ClassResolver.php
deleted file mode 100644
index ce96ff2b8fd..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/ClassResolver.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-/**
- * Class resolver for code-API command classes and other infrastructure classes.
- *
- * Plugins that implement their own API and want their command and infrastructure
- * classes instantiated through a container of their own can ship their own
- * ClassResolver class at `<plugin-api-namespace>\Infrastructure\ClassResolver`
- * with the same public signature: ApiBuilder detects it during generation
- * and routes the generated resolvers through it. When no such class is present,
- * resolvers fall back to `new $class_name()`.
- */
-final class ClassResolver {
- /**
- * Resolve a class to an instance.
- *
- * @param string $class_name Fully qualified name of the class to resolve.
- * @return object An instance of $class_name.
- */
- public static function resolve_class( string $class_name ): object {
- return wc_get_container()->get( $class_name );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/GraphQLControllerBase.php b/plugins/woocommerce/src/Api/Infrastructure/GraphQLControllerBase.php
deleted file mode 100644
index a44a12331d9..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/GraphQLControllerBase.php
+++ /dev/null
@@ -1,1084 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-use Automattic\WooCommerce\Api\Utils\SchemaHandle;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Internal\Api\QueryComplexityRule;
-use Automattic\WooCommerce\Internal\Api\QueryDepthRule;
-use Automattic\WooCommerce\Internal\Api\StatusResolverFailedException;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
-use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\DisableIntrospection;
-
-/**
- * Handles incoming GraphQL requests over the WooCommerce REST API.
- *
- * Abstract: the autogenerated `GraphQLController` subclass emitted by
- * ApiBuilder (both for WooCommerce core and for sibling plugins reusing this
- * infrastructure) is the concrete class. The public surface plugins extend
- * is engine-decoupled — the abstract {@see self::build_schema()} returns
- * {@see Schema}, a stable subclass of the underlying engine's schema type,
- * so a future engine swap doesn't break already-committed autogen trees.
- */
-abstract class GraphQLControllerBase {
- /**
- * Default nesting-depth limit applied when the option is unset or non-positive.
- *
- * Queries exceeding the configured limit are rejected during validation,
- * before any resolver runs. See {@see self::get_max_query_depth()} for the accessor.
- */
- public const DEFAULT_MAX_QUERY_DEPTH = 15;
-
- /**
- * Default complexity-score limit applied when the option is unset or non-positive.
- *
- * Complexity is the sum of per-field scores; connection fields multiply
- * their child score by the requested page size. Queries exceeding the
- * configured limit are rejected during validation. See
- * {@see self::get_max_query_complexity()} for the accessor.
- */
- public const DEFAULT_MAX_QUERY_COMPLEXITY = 1000;
-
- /**
- * Default path (relative to /wp-json/) at which the GraphQL route is registered.
- *
- * Used as the fallback when the {@see Main::OPTION_ENDPOINT_URL} option is
- * unset or was stored in an invalid form. See {@see self::get_endpoint_url()}
- * for the accessor.
- */
- public const DEFAULT_ENDPOINT_URL = 'wc/graphql';
-
- /**
- * Regex matching one valid path segment of the endpoint URL.
- *
- * Constrained to the character class WordPress REST routes accept
- * (alphanumerics, underscores, hyphens). Shared with {@see \Automattic\WooCommerce\Internal\Api\Settings::sanitize_endpoint_url()}
- * so the UI sanitizer and the controller-side fallback stay in lockstep.
- */
- public const ENDPOINT_URL_SEGMENT_PATTERN = '/^[A-Za-z0-9_\-]+$/';
-
- /**
- * Cached GraphQL schema instance.
- *
- * @var ?Schema
- */
- private ?Schema $schema = null;
-
- /**
- * Cached public-facing schema handle wrapping {@see self::$schema}.
- *
- * @var ?SchemaHandle
- */
- private ?SchemaHandle $schema_handle = null;
-
- /**
- * Query cache / APQ resolver.
- *
- * @var QueryCache
- */
- private QueryCache $query_cache;
-
- /**
- * Optional plugin-supplied HTTP status resolver.
- *
- * Populated from {@see self::get_status_resolver()} during {@see self::init()}.
- * Stays null when neither this controller nor its subclass supplies one,
- * in which case {@see self::pick_status()} short-circuits to the default
- * status without ever calling a resolver.
- *
- * Typed as `?object` rather than a WooCommerce-defined interface so that
- * sibling plugins do not have to import a WooCommerce type for what is
- * structurally a single duck-typed method.
- *
- * @var ?object
- */
- private ?object $status_resolver = null;
-
- /**
- * DI: injected by WooCommerce container.
- *
- * @internal
- * @param QueryCache $query_cache The query cache instance.
- */
- final public function init( QueryCache $query_cache ): void {
- $this->query_cache = $query_cache;
- // Resolved through a virtual hook so autogenerated subclasses can
- // supply a per-plugin resolver without changing init()'s signature.
- // Late binding picks up the override; init() can stay final.
- $this->status_resolver = $this->get_status_resolver();
- }
-
- /**
- * Return the HTTP status resolver instance to use for this controller, or
- * null to opt out. Default: null (use the framework defaults).
- *
- * Autogenerated subclasses override this when the plugin ships a
- * `<plugin-api-namespace>\Infrastructure\HttpStatusResolver` convention
- * class. The returned object is duck-typed: it must expose
- * `public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int`,
- * must return an int, and must not throw. A throw is treated as a plugin
- * bug and produces a fixed 500 INTERNAL_ERROR response — see
- * {@see self::pick_status()} and {@see self::handle_request()}.
- */
- protected function get_status_resolver(): ?object {
- return null;
- }
-
- /**
- * The maximum nesting depth allowed in a GraphQL query.
- *
- * Reads the {@see Main::OPTION_MAX_QUERY_DEPTH} store option; falls back
- * to {@see self::DEFAULT_MAX_QUERY_DEPTH} when the option is unset, empty,
- * or non-positive.
- */
- public static function get_max_query_depth(): int {
- $value = (int) get_option( Main::OPTION_MAX_QUERY_DEPTH, self::DEFAULT_MAX_QUERY_DEPTH );
- return $value > 0 ? $value : self::DEFAULT_MAX_QUERY_DEPTH;
- }
-
- /**
- * The maximum computed complexity score allowed for a GraphQL query.
- *
- * Reads the {@see Main::OPTION_MAX_QUERY_COMPLEXITY} store option; falls
- * back to {@see self::DEFAULT_MAX_QUERY_COMPLEXITY} when the option is
- * unset, empty, or non-positive.
- */
- public static function get_max_query_complexity(): int {
- $value = (int) get_option( Main::OPTION_MAX_QUERY_COMPLEXITY, self::DEFAULT_MAX_QUERY_COMPLEXITY );
- return $value > 0 ? $value : self::DEFAULT_MAX_QUERY_COMPLEXITY;
- }
-
- /**
- * The path (relative to /wp-json/) at which the GraphQL route is registered.
- *
- * Reads the {@see Main::OPTION_ENDPOINT_URL} store option; falls back to
- * {@see self::DEFAULT_ENDPOINT_URL} when the option is unset, empty, or
- * fails {@see self::is_valid_endpoint_url()}. The UI already validates on
- * save, so this defense-in-depth guard only fires for CLI-set option values.
- */
- public static function get_endpoint_url(): string {
- $value = trim( (string) get_option( Main::OPTION_ENDPOINT_URL, self::DEFAULT_ENDPOINT_URL ), '/' );
- if ( ! self::is_valid_endpoint_url( $value ) ) {
- return self::DEFAULT_ENDPOINT_URL;
- }
- return $value;
- }
-
- /**
- * Whether a value is a valid endpoint URL.
- *
- * Requires at least two non-empty path segments (so register_rest_route()
- * has both a namespace and a route), each matching
- * {@see self::ENDPOINT_URL_SEGMENT_PATTERN}. Mirrors the rules enforced on
- * save by {@see \Automattic\WooCommerce\Internal\Api\Settings::sanitize_endpoint_url()}, so values that bypass
- * the UI (e.g. CLI-set options) get the same treatment.
- *
- * @param string $value Endpoint URL with surrounding slashes already stripped.
- */
- private static function is_valid_endpoint_url( string $value ): bool {
- if ( '' === $value ) {
- return false;
- }
- $parts = explode( '/', $value );
- if ( count( $parts ) < 2 ) {
- return false;
- }
- foreach ( $parts as $part ) {
- if ( '' === $part || ! preg_match( self::ENDPOINT_URL_SEGMENT_PATTERN, $part ) ) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Split the endpoint URL into the `[namespace, route]` pair that
- * register_rest_route() expects.
- *
- * The last path segment becomes the route; everything before it becomes
- * the namespace. E.g. `wc/v4/graphql` → `['wc/v4', '/graphql']`.
- *
- * @return array{0: string, 1: string}
- */
- private static function split_endpoint_url(): array {
- $parts = explode( '/', self::get_endpoint_url() );
- $route = '/' . array_pop( $parts );
- $namespace = implode( '/', $parts );
- return array( $namespace, $route );
- }
-
- /**
- * Register the GraphQL REST route.
- */
- public function register(): void {
- $methods = Main::filter_methods_against_settings( array( 'GET', 'POST' ) );
- if ( empty( $methods ) ) {
- return;
- }
- list( $namespace, $route ) = self::split_endpoint_url();
-
- register_rest_route(
- $namespace,
- $route,
- array(
- 'methods' => $methods,
- 'callback' => array( $this, 'handle_request' ),
- // Auth is handled per-query/mutation.
- 'permission_callback' => '__return_true',
- )
- );
- }
-
- /**
- * Handle an incoming GraphQL request.
- *
- * Resolves the principal first so debug-mode / introspection checks can
- * consult it from inside both `process_request()` and the top-level
- * exception formatter. When `resolve_request_principal()` itself throws
- * (e.g. an InvalidTokenException from a plugin's PrincipalResolver),
- * `$principal` stays null and the resulting error response carries no
- * debug info — by design, since the caller failed to authenticate.
- *
- * @param \WP_REST_Request $request The REST request.
- */
- public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
- $principal = null;
- try {
- $principal = $this->resolve_request_principal( $request );
- return $this->process_request( $request, $principal );
- } catch ( StatusResolverFailedException $e ) {
- // Resolver threw on one of the decision points inside
- // process_request(). Produce a clean 500 without re-invoking
- // the (broken) resolver.
- return $this->build_resolver_failure_response( $e, $request, $principal );
- } catch ( \Throwable $e ) {
- $output = array(
- 'errors' => array(
- $this->format_exception( $e, $request, $principal ),
- ),
- );
-
- $default = $this->get_error_status( $output['errors'] );
- try {
- $status = $this->pick_status( $default, $output, $request );
- } catch ( StatusResolverFailedException $e2 ) {
- // Resolver threw specifically when handed the synthetic
- // errors shape from this catch block. Fall through to the
- // fixed 500; do not loop back into the resolver.
- return $this->build_resolver_failure_response( $e2, $request, $principal );
- }
-
- return new \WP_REST_Response( $output, $status );
- }
- }
-
- /**
- * Build the canonical 500 response used when the HTTP status resolver
- * throws or returns an out-of-range value. Body shape matches an
- * unhandled internal error so callers don't need a separate path for
- * "resolver blew up".
- *
- * In debug mode, attaches `extensions.debug` (message, file, line, trace)
- * for the wrapper exception, plus an `extensions.previous` chain when the
- * resolver itself threw — mirroring the shape that {@see self::format_exception()}
- * produces for the generic-Throwable path. Outside debug mode the body
- * stays purely generic so resolver internals never leak to anonymous callers.
- *
- * @param StatusResolverFailedException $e The wrapper exception thrown by {@see self::pick_status()}.
- * @param \WP_REST_Request $request The originating REST request.
- * @param ?object $principal The resolved principal, or null when resolution failed.
- */
- private function build_resolver_failure_response(
- StatusResolverFailedException $e,
- \WP_REST_Request $request,
- ?object $principal
- ): \WP_REST_Response {
- $error = array(
- 'message' => 'An unexpected error occurred.',
- 'extensions' => array( 'code' => 'INTERNAL_ERROR' ),
- );
-
- if ( $this->is_debug_mode( $principal, $request ) ) {
- $error['extensions']['debug'] = array(
- 'message' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString(),
- );
-
- $chain = $this->extract_previous_chain( $e );
- if ( ! empty( $chain ) ) {
- $error['extensions']['previous'] = $chain;
- }
- }
-
- return new \WP_REST_Response( array( 'errors' => array( $error ) ), 500 );
- }
-
- /**
- * Filter the framework-computed default HTTP status through the optional
- * plugin-supplied status resolver.
- *
- * When no resolver is configured this returns the default verbatim. When
- * a resolver is configured its return value (an int) is returned in
- * place of the default — which the resolver may also pass through
- * unchanged for cases it does not want to override.
- *
- * Resolver-thrown exceptions are converted into an internal
- * {@see StatusResolverFailedException} for {@see self::handle_request()}
- * to handle, so a plugin bug never corrupts or duplicates a response.
- *
- * @param int $default The framework-computed default status.
- * @param array $output The response body about to be sent (may include `errors`/`data`).
- * @param \WP_REST_Request $request The originating request.
- *
- * @throws StatusResolverFailedException When the resolver throws or returns a status code outside the 100..599 HTTP range.
- */
- private function pick_status( int $default, array $output, \WP_REST_Request $request ): int {
- if ( null === $this->status_resolver ) {
- return $default;
- }
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal sentinel; never serialised to the wire.
- try {
- $resolved = $this->status_resolver->resolve_status( $default, $output, $request );
- } catch ( \Throwable $e ) {
- throw new StatusResolverFailedException( 'HTTP status resolver threw.', 0, $e );
- }
- // Guard against nonsensical return values. Range-checking outside the
- // try/catch keeps this exception out of the generic-Throwable wrap
- // above, so a bad return value surfaces as the same fixed-shape 500
- // response as a throw — never as a malformed WP_REST_Response.
- if ( $resolved < 100 || $resolved > 599 ) {
- throw new StatusResolverFailedException(
- sprintf( 'HTTP status resolver returned an out-of-range status code: %d.', $resolved )
- );
- }
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- return $resolved;
- }
-
- /**
- * Process the GraphQL request. Extracted so that handle_request() can
- * wrap everything in a single try/catch that respects debug mode.
- *
- * @param \WP_REST_Request $request The REST request.
- * @param object $principal The principal resolved by handle_request(); never null when this is reached.
- */
- private function process_request( \WP_REST_Request $request, object $principal ): \WP_REST_Response {
- // 2. Parse request. GET query-string `variables` and `extensions`
- // arrive as JSON strings; decode_json_param() unifies them with the
- // already-decoded-array path from POST bodies and rejects malformed
- // or non-object payloads up front so they surface as HTTP 400
- // INVALID_ARGUMENT instead of as confusing resolver errors (null
- // decode) or HTTP 500 TypeErrors (scalar decode).
- $query = $request->get_param( 'query' );
- $operation_name = $request->get_param( 'operationName' );
- $variables = $this->decode_json_param( $request->get_param( 'variables' ), 'variables' );
- $extensions = $this->decode_json_param( $request->get_param( 'extensions' ), 'extensions' );
-
- // 3. Resolve query (cache lookup / APQ / parse).
- $source = $this->query_cache->resolve( $query, $extensions );
- if ( is_array( $source ) ) {
- $default = $this->get_resolve_error_status( $source );
- return new \WP_REST_Response( $source, $this->pick_status( $default, $source, $request ) );
- }
-
- // 4. Reject mutations over GET (GraphQL over HTTP spec).
- if ( 'GET' === $request->get_method() && $this->document_has_mutation( $source, $operation_name ) ) {
- $method_not_allowed_output = array(
- 'errors' => array(
- array(
- 'message' => 'Mutations are not allowed over GET requests. Use POST instead.',
- 'extensions' => array( 'code' => 'METHOD_NOT_ALLOWED' ),
- ),
- ),
- );
- return new \WP_REST_Response(
- $method_not_allowed_output,
- $this->pick_status( 405, $method_not_allowed_output, $request )
- );
- }
-
- // 5. Load schema.
- $schema = $this->get_engine_schema();
-
- // 6. Build validation rules.
- // A single complexity-rule instance is kept so its computed score can
- // be surfaced in the debug extensions after execution.
- $complexity_rule = new QueryComplexityRule( self::get_max_query_complexity() );
- $validation_rules = array_values( DocumentValidator::allRules() );
- $validation_rules[] = new QueryDepthRule( self::get_max_query_depth() );
- $validation_rules[] = $complexity_rule;
- if ( ! $this->is_introspection_allowed( $principal, $request ) ) {
- $validation_rules[] = new DisableIntrospection( DisableIntrospection::ENABLED );
- }
-
- // 7. Execute. The context value is an ArrayObject (not a plain array)
- // so root resolvers can mutate it — specifically to thread the root
- // query's metadata into `$context['_query_metadata']` for downstream
- // field-level authorization gates. ArrayObject preserves the
- // `$context['key']` read syntax via ArrayAccess. The context carries
- // the resolved principal through to autogenerated resolvers, which
- // expose it as the `_principal` infrastructure parameter when commands
- // declare it on their authorize()/execute() methods. Request-derived
- // data that resolvers need is carried by the principal class itself —
- // populated by the PrincipalResolver, the only component wired to the
- // HTTP transport.
- $result = GraphQL::executeQuery(
- schema: $schema,
- source: $source,
- contextValue: new \ArrayObject(
- array(
- 'principal' => $principal,
- )
- ),
- variableValues: $variables,
- operationName: $operation_name,
- validationRules: $validation_rules,
- );
-
- // Install an error formatter that guarantees every error carries an
- // `extensions.code`. Our resolvers route everything through
- // Utils::execute_command / Utils::authorize_command, which already
- // translate domain exceptions (ApiException, InvalidArgumentException,
- // generic Throwable) into coded GraphQL errors at the throw site.
- // What reaches us uncoded here is webonyx-native validation and
- // execution output, so we infer from webonyx's ClientAware signal:
- // client-safe errors become BAD_USER_INPUT (400), the rest become
- // INTERNAL_ERROR (500).
- //
- // In debug mode the same formatter also walks the previous-exception
- // chain so wrapped errors (e.g. a \ValueError caught by a resolver and
- // re-thrown as INTERNAL_ERROR) stay visible to the developer instead
- // of being masked behind the generic "Internal server error" message.
- $debug_mode = $this->is_debug_mode( $principal, $request );
- $result->setErrorFormatter(
- function ( \Throwable $error ) use ( $debug_mode ): array {
- $formatted = \Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError::createFromException( $error );
-
- if ( ! isset( $formatted['extensions']['code'] ) ) {
- $client_safe = $error instanceof \Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware && $error->isClientSafe();
- $formatted['extensions']['code'] = $client_safe ? 'BAD_USER_INPUT' : 'INTERNAL_ERROR';
- }
-
- // SerializationError (thrown during schema-type coercion, e.g. when
- // a resolver returns an Int that doesn't fit 32 bits) extends
- // \Exception rather than webonyx's ClientAware Error, so it lands
- // in the INTERNAL_ERROR bucket above. Its message is actually
- // client-actionable ("value out of range — send smaller inputs"),
- // so promote it to BAD_USER_INPUT when it shows up anywhere in
- // the previous-exception chain.
- if ( 'BAD_USER_INPUT' !== ( $formatted['extensions']['code'] ?? null ) ) {
- $cursor = $error;
- while ( $cursor instanceof \Throwable ) {
- if ( $cursor instanceof \Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError ) {
- $formatted['extensions']['code'] = 'BAD_USER_INPUT';
- break;
- }
- $cursor = $cursor->getPrevious();
- }
- }
-
- if ( $debug_mode ) {
- $chain = $this->extract_previous_chain( $error );
- if ( ! empty( $chain ) ) {
- $formatted['extensions']['previous'] = $chain;
- }
- }
-
- return $formatted;
- }
- );
-
- $debug_flags = $this->get_debug_flags( $request, $principal );
- $output = $result->toArray( $debug_flags );
-
- // 8. Debug-mode metrics: expose the computed complexity and depth so
- // clients tuning queries can see what the server scored the request at.
- if ( $this->is_debug_mode( $principal, $request ) ) {
- if ( ! isset( $output['extensions'] ) ) {
- $output['extensions'] = array();
- }
- if ( ! isset( $output['extensions']['debug'] ) ) {
- $output['extensions']['debug'] = array();
- }
- $output['extensions']['debug']['complexity'] = $complexity_rule->getQueryComplexity();
- $output['extensions']['debug']['depth'] = $this->compute_query_depth( $source, $operation_name );
- }
-
- // 9. Determine HTTP status code. GraphQL emits `data: { field: null }`
- // for nullable root fields even when the resolver errored, so gating
- // the status override on `data` being absent would leave nearly every
- // error response on HTTP 200. Always derive the status from the
- // errors array when one is present — clients that need "200 with
- // partial data" semantics can still read the `errors` array.
- $default = isset( $output['errors'] ) ? $this->get_error_status( $output['errors'] ) : 200;
- $status = $this->pick_status( $default, $output, $request );
-
- return new \WP_REST_Response( $output, $status );
- }
-
- /**
- * Public handle to the live GraphQL schema for runtime inspection.
- *
- * Returns an opaque {@see SchemaHandle}; callers reach metadata (and any
- * future schema-inspection operations) through methods on that object
- * rather than touching the underlying engine type. The handle is cached
- * and wraps the same engine schema this controller uses to serve real
- * requests.
- */
- public function get_schema(): SchemaHandle {
- if ( null === $this->schema_handle ) {
- $this->schema_handle = new SchemaHandle( $this->get_engine_schema() );
- }
- return $this->schema_handle;
- }
-
- /**
- * Build and cache the engine-typed GraphQL schema used internally to
- * serve requests. Kept private to keep the engine type out of the
- * controller's public surface; consumers should reach {@see SchemaHandle}
- * through {@see self::get_schema()} instead.
- */
- private function get_engine_schema(): Schema {
- if ( null === $this->schema ) {
- $this->schema = $this->build_schema();
- }
- return $this->schema;
- }
-
- /**
- * Construct the GraphQL schema.
- *
- * Implemented by the autogenerated subclass emitted by ApiBuilder
- * (both for WooCommerce core and for sibling plugins that reuse this
- * infrastructure) so the base class stays agnostic to any specific
- * autogenerated namespace.
- */
- abstract protected function build_schema(): Schema;
-
- /**
- * FQCN of the user-provided ClassResolver, or null when none was detected.
- *
- * The autogenerated subclass overrides this to return its plugin's
- * `<api_namespace>\Infrastructure\ClassResolver` when ApiBuilder detected
- * one. When null, classes are instantiated with `new $class()`.
- */
- protected function get_class_resolver_fqcn(): ?string {
- return null;
- }
-
- /**
- * FQCN of the user-provided PrincipalResolver, or null when none was detected.
- *
- * The autogenerated subclass overrides this to return its plugin's
- * `<api_namespace>\Infrastructure\PrincipalResolver` when ApiBuilder detected
- * one. When null, the controller falls back to {@see \wp_get_current_user()}
- * (anonymous → null) to populate the request principal.
- */
- protected function get_principal_resolver_fqcn(): ?string {
- return null;
- }
-
- /**
- * Whether the configured PrincipalResolver's `resolve_principal()` declares
- * the \WP_REST_Request parameter (true) or omits it (false).
- *
- * Captured at build time and emitted as an override on the autogenerated
- * controller subclass, so the call below uses the right arity without
- * runtime reflection. Default is irrelevant when {@see self::get_principal_resolver_fqcn()}
- * returns null (the inline `wp_get_current_user()` fallback applies instead).
- */
- protected function principal_resolver_takes_request(): bool {
- return false;
- }
-
- /**
- * Resolve a class to an instance via the configured ClassResolver, or `new`.
- *
- * Used internally to instantiate the PrincipalResolver per request. The
- * autogenerated resolver classes use the detected ClassResolver directly
- * (the FQCN is baked in at build time), so this helper is only the runtime
- * path for infrastructure classes that the controller itself has to load.
- *
- * @param string $class_name Fully-qualified name of the class to resolve.
- */
- private function resolve_class( string $class_name ): object {
- $resolver = $this->get_class_resolver_fqcn();
- if ( null === $resolver ) {
- return new $class_name();
- }
- return $resolver::resolve_class( $class_name );
- }
-
- /**
- * Resolve the request principal once per HTTP request.
- *
- * Invoked eagerly at the top of {@see self::process_request()}, so a
- * principal resolver throwing {@see ApiException} fails the request before
- * any resolver runs (single coded error in the response, no `data`).
- *
- * The principal is never null — anonymous requests are signalled by a
- * principal whose authentication state is unauthenticated (for the default
- * {@see \Automattic\WooCommerce\Api\Infrastructure\Principal}, that's
- * `Principal::$user->ID === 0`). Plugin resolvers can also signal "invalid
- * credentials" by throwing ApiException.
- *
- * The configured resolver's `resolve_principal()` may declare its
- * \WP_REST_Request parameter or omit it; the autogenerated subclass
- * overrides {@see self::principal_resolver_takes_request()} so this call
- * uses the right arity without runtime reflection.
- *
- * @param \WP_REST_Request $request The incoming REST request.
- * @throws ApiException When the configured PrincipalResolver rejects the request.
- */
- private function resolve_request_principal( \WP_REST_Request $request ): object {
- $fqcn = $this->get_principal_resolver_fqcn();
- if ( null === $fqcn ) {
- return new \Automattic\WooCommerce\Api\Infrastructure\Principal( wp_get_current_user() );
- }
- $resolver = $this->resolve_class( $fqcn );
- return $this->principal_resolver_takes_request()
- ? $resolver->resolve_principal( $request )
- : $resolver->resolve_principal();
- }
-
- /**
- * Decode an optional JSON-object param (`variables` / `extensions`) into an array.
- *
- * WP_REST_Request delivers POST-body params as already-decoded arrays,
- * but GET query-string equivalents arrive as raw JSON strings. This
- * helper unifies the two and rejects malformed JSON or non-object
- * payloads with an InvalidArgumentException — which handle_request()
- * surfaces as HTTP 400 INVALID_ARGUMENT, rather than letting a null
- * decode slip through as "no variables" or a scalar decode trigger a
- * downstream TypeError / HTTP 500.
- *
- * @param mixed $value The param value from WP_REST_Request::get_param().
- * @param string $name The param name, used in error messages.
- * @return array The decoded object, or an empty array when the param is omitted / empty / JSON null.
- * @throws \InvalidArgumentException When the payload is not a JSON object or not valid JSON.
- */
- private function decode_json_param( $value, string $name ): array {
- if ( null === $value ) {
- return array();
- }
- if ( is_array( $value ) ) {
- return $value;
- }
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- if ( ! is_string( $value ) ) {
- throw new \InvalidArgumentException(
- sprintf( 'Argument `%s` must be a JSON object or omitted.', $name )
- );
- }
- if ( '' === $value ) {
- return array();
- }
- $decoded = json_decode( $value, true );
- if ( JSON_ERROR_NONE !== json_last_error() ) {
- throw new \InvalidArgumentException(
- sprintf( 'Argument `%s` is not valid JSON: %s', $name, json_last_error_msg() )
- );
- }
- if ( null === $decoded ) {
- // Literal "null" JSON payload — treat as omitted.
- return array();
- }
- if ( ! is_array( $decoded ) ) {
- throw new \InvalidArgumentException(
- sprintf( 'Argument `%s` must be a JSON object (got %s).', $name, gettype( $decoded ) )
- );
- }
- return $decoded;
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
-
- /**
- * Determine debug flags for the request, based on {@see self::is_debug_mode()}.
- *
- * @param \WP_REST_Request $request The REST request.
- * @param ?object $principal The resolved principal, or null if resolution itself failed.
- */
- private function get_debug_flags( \WP_REST_Request $request, ?object $principal ): int {
- if ( ! $this->is_debug_mode( $principal, $request ) ) {
- return DebugFlag::NONE;
- }
- return DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
- }
-
- /**
- * Check whether GraphQL introspection is allowed for this request.
- *
- * The principal opts in via a `can_introspect(): bool` method; principals
- * that don't declare it are denied by default. The decision is then passed
- * through the {@see 'woocommerce_graphql_can_introspect'} filter so sites
- * can grant or revoke access without subclassing the principal — useful
- * for per-request rules (specific IPs, headers, query parameters, etc.).
- *
- * Fail-closed contract: the principal must be non-null (principal-resolution
- * failures deny outright, before the filter is consulted), the principal
- * method's return value is treated with `=== true`, and any throw from
- * either the principal method or the filter callback denies. The filter
- * must likewise return strictly `true` to allow; any other value denies.
- *
- * @param ?object $principal The resolved principal, or null when principal resolution failed.
- * @param \WP_REST_Request $request The REST request.
- */
- private function is_introspection_allowed( ?object $principal, \WP_REST_Request $request ): bool {
- if ( is_null( $principal ) ) {
- return false;
- }
-
- try {
- $can_introspect = method_exists( $principal, 'can_introspect' )
- && true === $principal->can_introspect();
-
- /**
- * Filters whether the current principal may run GraphQL introspection.
- *
- * The filter receives the principal-derived decision (false when the
- * principal doesn't declare `can_introspect()` or its `can_introspect()`
- * doesn't return strictly `true`) and must return strictly `true` to
- * grant access; any other return value denies. The filter is not
- * invoked when principal resolution failed (i.e. when the controller
- * passes a null principal) — that case denies outright.
- *
- * @since 10.9.0
- *
- * @internal
- *
- * @param bool $can_introspect Whether the principal can introspect, derived from `$principal->can_introspect()`.
- * @param object $principal The resolved principal.
- * @param \WP_REST_Request $request The REST request being processed.
- */
- $can_introspect = apply_filters( 'woocommerce_graphql_can_introspect', $can_introspect, $principal, $request );
- } catch ( \Throwable $e ) {
- return false;
- }
-
- return true === $can_introspect;
- }
-
- /**
- * Check if debug mode is active.
- *
- * Debug mode is gated on `_debug=1` being set on the request: when absent,
- * debug mode is off regardless of any other signal. When present, the
- * principal opts in via a `can_use_debug_mode(): bool` method (principals
- * that don't declare it are denied by default), and the decision is then
- * passed through the {@see 'woocommerce_graphql_can_use_debug_mode'} filter.
- *
- * Fail-closed contract: the principal must be non-null (principal-resolution
- * failures deny outright, before the filter is consulted), the principal
- * method's return value is treated with `=== true`, and any throw from
- * either the principal method or the filter callback denies. The filter
- * must likewise return strictly `true` to allow; any other value denies.
- *
- * @param ?object $principal The resolved principal, or null when principal resolution failed.
- * @param \WP_REST_Request $request The REST request.
- */
- private function is_debug_mode( ?object $principal, \WP_REST_Request $request ): bool {
- if ( '1' !== $request->get_param( '_debug' ) ) {
- return false;
- }
- if ( is_null( $principal ) ) {
- return false;
- }
-
- try {
- $can_debug = method_exists( $principal, 'can_use_debug_mode' )
- && true === $principal->can_use_debug_mode();
-
- /**
- * Filters whether the current principal may activate GraphQL debug mode.
- *
- * Only invoked when the request carries `_debug=1` and a principal was
- * resolved successfully, so the filter is not called on every GraphQL
- * request. The filter receives the principal-derived decision (false
- * when the principal doesn't declare `can_use_debug_mode()` or its
- * `can_use_debug_mode()` doesn't return strictly `true`) and must
- * return strictly `true` to grant access; any other return value denies.
- *
- * @since 10.9.0
- *
- * @internal
- *
- * @param bool $can_debug Whether the principal can use debug mode, derived from `$principal->can_use_debug_mode()`.
- * @param object $principal The resolved principal.
- * @param \WP_REST_Request $request The REST request being processed.
- */
- $can_debug = apply_filters( 'woocommerce_graphql_can_use_debug_mode', $can_debug, $principal, $request );
- } catch ( \Throwable $e ) {
- return false;
- }
-
- return true === $can_debug;
- }
-
- /**
- * Format a caught exception into a GraphQL error array.
- *
- * @param \Throwable $e The caught exception.
- * @param \WP_REST_Request $request The REST request.
- * @param ?object $principal The resolved principal, or null when the exception came from principal resolution itself.
- */
- private function format_exception( \Throwable $e, \WP_REST_Request $request, ?object $principal ): array {
- if ( $e instanceof ApiException ) {
- // Caller-supplied extensions come first so the canonical
- // getErrorCode() can't be silently overridden by an extensions
- // entry keyed 'code'. Mirrors the same invariant enforced by
- // Utils::translate_exceptions() for the execute/authorize paths.
- $error = array(
- 'message' => $e->getMessage(),
- 'extensions' => array_merge(
- $e->getExtensions(),
- array( 'code' => $e->getErrorCode() )
- ),
- );
- } elseif ( $e instanceof \InvalidArgumentException ) {
- $error = array(
- 'message' => $e->getMessage(),
- 'extensions' => array( 'code' => 'INVALID_ARGUMENT' ),
- );
- } else {
- $error = array(
- 'message' => 'An unexpected error occurred.',
- 'extensions' => array( 'code' => 'INTERNAL_ERROR' ),
- );
- }
-
- if ( $this->is_debug_mode( $principal, $request ) ) {
- $error['extensions']['debug'] = array(
- 'message' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString(),
- );
-
- $chain = $this->extract_previous_chain( $e );
- if ( ! empty( $chain ) ) {
- $error['extensions']['debug']['previous'] = $chain;
- }
- }
-
- return $error;
- }
-
- /**
- * Walk the `getPrevious()` chain of a Throwable and return one entry per
- * wrapped exception. Used in debug mode so that resolver-level wrappers
- * (which bury the real cause behind a generic "INTERNAL_ERROR") still
- * surface the underlying class/message/file/line/trace.
- *
- * @param \Throwable $e The outermost exception.
- * @return array<int, array{class: string, message: string, file: string, line: int, trace: string[]}>
- */
- private function extract_previous_chain( \Throwable $e ): array {
- $chain = array();
- for ( $prev = $e->getPrevious(); null !== $prev; $prev = $prev->getPrevious() ) {
- $chain[] = array(
- 'class' => get_class( $prev ),
- 'message' => $prev->getMessage(),
- 'file' => $prev->getFile(),
- 'line' => $prev->getLine(),
- 'trace' => explode( "\n", $prev->getTraceAsString() ),
- );
- }
- return $chain;
- }
-
- /**
- * Mapping from machine-readable error codes to HTTP status codes.
- *
- * Any code not listed here defaults to 500, so unknown/unrecognised codes
- * from third-party resolvers stay on the safe side. The error formatter
- * installed in process_request() guarantees every error carries a code
- * from this table before get_error_status() inspects it.
- */
- private const ERROR_STATUS_MAP = array(
- 'UNAUTHORIZED' => 401,
- 'INVALID_TOKEN' => 401,
- 'FORBIDDEN' => 403,
- 'NOT_FOUND' => 404,
- 'METHOD_NOT_ALLOWED' => 405,
- 'INVALID_ARGUMENT' => 400,
- 'BAD_USER_INPUT' => 400,
- 'GRAPHQL_PARSE_ERROR' => 400,
- 'GRAPHQL_PARSE_FAILED' => 400,
- 'GRAPHQL_VALIDATION_FAILED' => 400,
- 'VALIDATION_ERROR' => 422,
- 'INTERNAL_ERROR' => 500,
- );
-
- /**
- * Determine the HTTP status code from an array of GraphQL errors.
- *
- * Applies the code-to-status lookup to each error and returns the worst
- * (highest) status seen. A single genuine 5xx among mixed errors surfaces
- * as 500, which is the more useful signal for monitoring and logs.
- *
- * @param array $errors The GraphQL errors array.
- */
- private function get_error_status( array $errors ): int {
- $status = 200;
- foreach ( $errors as $error ) {
- $code = $error['extensions']['code'] ?? null;
- $mapped = self::ERROR_STATUS_MAP[ $code ] ?? 500;
- if ( $mapped > $status ) {
- $status = $mapped;
- }
- }
- return $status;
- }
-
- /**
- * Determine the HTTP status code for an error returned by QueryCache::resolve().
- *
- * PERSISTED_QUERY_NOT_FOUND uses 200 per the Apollo APQ convention (protocol signal, not error).
- *
- * @param array $response The error response array from resolve().
- */
- private function get_resolve_error_status( array $response ): int {
- $code = $response['errors'][0]['extensions']['code'] ?? '';
-
- if ( 'PERSISTED_QUERY_NOT_FOUND' === $code ) {
- return 200;
- }
-
- return 400;
- }
-
- /**
- * Compute the maximum nesting depth of the executing operation, under two
- * different metrics:
- *
- * - `tree_only`: only fields whose own selection set is non-empty count
- * toward depth; leaves are excluded. This is the number directly
- * comparable to the "Maximum query depth" setting's limit, and matches
- * what webonyx's QueryDepth validation rule measures for the enforcement
- * decision.
- * - `in_depth`: counts every field in the deepest chain, leaves included.
- * Useful as a shape metric when inspecting a query.
- *
- * Inline fragments pass through without incrementing either metric.
- * Named-fragment spreads are not expanded here, so both numbers are lower
- * bounds when spreads are present. The webonyx QueryDepth validation rule
- * (which does expand spreads) remains the authoritative gate.
- *
- * @param DocumentNode $document The parsed GraphQL document.
- * @param ?string $operation_name The requested operation name, if any.
- * @return array{tree_only: int, in_depth: int}
- */
- private function compute_query_depth( DocumentNode $document, ?string $operation_name ): array {
- $tree_only = 0;
- $in_depth = 0;
- foreach ( $document->definitions as $definition ) {
- if ( ! $definition instanceof OperationDefinitionNode ) {
- continue;
- }
-
- if ( null !== $operation_name && ( $definition->name->value ?? null ) !== $operation_name ) {
- continue;
- }
-
- $tree_only = max( $tree_only, $this->walk_depth_tree_only( $definition->selectionSet, 0 ) );
- $in_depth = max( $in_depth, $this->walk_depth_in_depth( $definition->selectionSet, 0 ) );
- }
-
- return array(
- 'tree_only' => $tree_only,
- 'in_depth' => $in_depth,
- );
- }
-
- /**
- * Walk a selection set counting only fields with child selections, matching
- * webonyx's QueryDepth rule so the returned number is directly comparable
- * to the configured "Maximum query depth" limit.
- *
- * @param ?SelectionSetNode $selection_set The selection set to walk.
- * @param int $depth The depth at which fields in this selection set sit.
- */
- private function walk_depth_tree_only( ?SelectionSetNode $selection_set, int $depth ): int {
- if ( null === $selection_set ) {
- return 0;
- }
-
- $max = 0;
- foreach ( $selection_set->selections as $selection ) {
- if ( $selection instanceof FieldNode ) {
- if ( null !== $selection->selectionSet ) {
- $max = max( $max, $depth, $this->walk_depth_tree_only( $selection->selectionSet, $depth + 1 ) );
- }
- } elseif ( $selection instanceof InlineFragmentNode ) {
- $max = max( $max, $this->walk_depth_tree_only( $selection->selectionSet, $depth ) );
- }
- }
-
- return $max;
- }
-
- /**
- * Walk a selection set counting every field in the deepest chain, leaves
- * included. Produces the "shape" metric surfaced alongside the enforcement
- * metric in debug output.
- *
- * @param ?SelectionSetNode $selection_set The selection set to walk, or null for a leaf.
- * @param int $depth The depth of the selection set's parent.
- */
- private function walk_depth_in_depth( ?SelectionSetNode $selection_set, int $depth ): int {
- if ( null === $selection_set ) {
- return $depth;
- }
-
- $max = $depth;
- foreach ( $selection_set->selections as $selection ) {
- if ( $selection instanceof FieldNode ) {
- $max = max( $max, $this->walk_depth_in_depth( $selection->selectionSet, $depth + 1 ) );
- } elseif ( $selection instanceof InlineFragmentNode ) {
- $max = max( $max, $this->walk_depth_in_depth( $selection->selectionSet, $depth ) );
- }
- }
-
- return $max;
- }
-
- /**
- * Check whether the parsed document contains a mutation operation.
- *
- * When an operation name is given, only that operation is checked;
- * otherwise any mutation definition in the document triggers a match.
- *
- * @param DocumentNode $document The parsed GraphQL document.
- * @param ?string $operation_name The requested operation name, if any.
- */
- private function document_has_mutation( DocumentNode $document, ?string $operation_name ): bool {
- foreach ( $document->definitions as $definition ) {
- if ( ! $definition instanceof OperationDefinitionNode ) {
- continue;
- }
-
- if ( null !== $operation_name && ( $definition->name->value ?? null ) !== $operation_name ) {
- continue;
- }
-
- if ( 'mutation' === $definition->operation ) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Main.php b/plugins/woocommerce/src/Api/Infrastructure/Main.php
deleted file mode 100644
index 6cce34fea27..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Main.php
+++ /dev/null
@@ -1,415 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated;
-use Automattic\WooCommerce\Internal\Api\GraphQLEndpointRegistrar;
-use Automattic\WooCommerce\Internal\Api\OpcacheFileExpiry;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Internal\Api\Settings;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;
-
-/**
- * Entry point for the WooCommerce GraphQL API.
- *
- * This class is intentionally free of PHP 8.0+ syntax so that it can be
- * loaded and called on PHP 7.4 without parse errors. The PHP-8.1-only
- * classes (GraphQLControllerBase, QueryCache, etc.) are resolved lazily
- * from the DI container only after is_enabled() confirms PHP 8.1+ is
- * available.
- */
-class Main {
- /**
- * Feature flag slug registered in FeaturesController.
- */
- private const FEATURE_SLUG = 'dual_code_graphql_api';
-
- /**
- * Option name for the "Enable GET endpoint" setting.
- *
- * When disabled, the GraphQL route only accepts POST requests.
- */
- public const OPTION_GET_ENDPOINT_ENABLED = 'woocommerce_graphql_get_endpoint_enabled';
-
- /**
- * Option name for the "Enable APQ caching" setting.
- *
- * When disabled, the persistedQuery extension is ignored and requests are
- * treated as standard queries.
- */
- public const OPTION_APQ_ENABLED = 'woocommerce_graphql_apq_enabled';
-
- /**
- * Option name for the "Endpoint URL" setting.
- *
- * Path (relative to /wp-json/) at which the GraphQL route is registered.
- */
- public const OPTION_ENDPOINT_URL = 'woocommerce_graphql_endpoint_url';
-
- /**
- * Option name for the "Maximum query depth" setting.
- *
- * Caps how deep the selection tree of a GraphQL query may nest.
- */
- public const OPTION_MAX_QUERY_DEPTH = 'woocommerce_graphql_max_query_depth';
-
- /**
- * Option name for the "Maximum query complexity" setting.
- *
- * Caps the computed complexity score of a GraphQL query — connection
- * fields multiply their children's cost by the requested page size.
- */
- public const OPTION_MAX_QUERY_COMPLEXITY = 'woocommerce_graphql_max_query_complexity';
-
- /**
- * Option name for the "OPcache-based caching" setting.
- *
- * When enabled, parsed query ASTs are written to disk as PHP files so
- * that OPcache serves them from shared memory on subsequent requests.
- */
- public const OPTION_OPCACHE_ENABLED = 'woocommerce_graphql_opcache_enabled';
-
- /**
- * Option name for the "ObjectCache-based caching" setting.
- */
- public const OPTION_OBJECT_CACHE_ENABLED = 'woocommerce_graphql_object_cache_enabled';
-
- /**
- * Option name for the "Parsed query cache TTL" setting.
- *
- * Time-to-live (in seconds) applied to parsed-query AST entries written
- * to the WP object cache by both the standard-query and APQ paths.
- */
- public const OPTION_QUERY_CACHE_TTL = 'woocommerce_graphql_query_cache_ttl';
-
- /**
- * Check whether the Dual Code & GraphQL API feature is active.
- *
- * Requires PHP 8.1+ and the dual_code_graphql_api feature flag to be
- * enabled.
- *
- * @return bool
- */
- public static function is_enabled(): bool {
- return PHP_VERSION_ID >= 80100 && FeaturesUtil::feature_is_enabled( self::FEATURE_SLUG );
- }
-
- /**
- * Whether the GraphQL endpoint accepts GET requests.
- *
- * Defaults to false. Reads from the option written by the GraphQL
- * settings section so the REST route registration can decide which
- * HTTP methods to accept.
- */
- public static function is_get_endpoint_enabled(): bool {
- return wc_string_to_bool( get_option( self::OPTION_GET_ENDPOINT_ENABLED, 'yes' ) );
- }
-
- /**
- * Whether the Apollo Automatic Persisted Queries (APQ) protocol is enabled.
- *
- * Defaults to true. When disabled, the `persistedQuery` request extension
- * is ignored and requests are processed as standard (non-persisted) queries.
- */
- public static function is_apq_enabled(): bool {
- return wc_string_to_bool( get_option( self::OPTION_APQ_ENABLED, 'yes' ) );
- }
-
- /**
- * Whether the OPcache-backed query cache is enabled.
- *
- * Defaults to true. Activation also depends on OPcache being loaded and
- * the cache directory being writable; see {@see QueryCache} for the
- * runtime capability check.
- */
- public static function is_opcache_enabled(): bool {
- return wc_string_to_bool( get_option( self::OPTION_OPCACHE_ENABLED, 'yes' ) );
- }
-
- /**
- * Whether the ObjectCache-backed query cache is enabled.
- *
- * Defaults to true.
- */
- public static function is_object_cache_enabled(): bool {
- return wc_string_to_bool( get_option( self::OPTION_OBJECT_CACHE_ENABLED, 'yes' ) );
- }
-
- /**
- * Apply the GraphQL-scoped site settings to a caller-declared list of HTTP
- * methods.
- *
- * Centralises the "what verbs does the admin actually allow on a GraphQL
- * endpoint" rule so both WooCommerce core's own endpoint and every sibling
- * plugin endpoint applied through {@see self::register_graphql_endpoint()}
- * honour the same settings.
- *
- * Currently the only rule is: if the GET endpoint has been disabled in the
- * GraphQL settings section, strip GET from the list.
- *
- * @param string[] $methods HTTP methods the caller declared.
- * @return string[] Possibly narrowed list — may be empty, in which case the
- * caller should skip the route registration entirely.
- */
- public static function filter_methods_against_settings( array $methods ): array {
- if ( ! self::is_get_endpoint_enabled() ) {
- $methods = array_values( array_diff( $methods, array( 'GET' ) ) );
- }
- return $methods;
- }
-
- /**
- * Register the GraphQL endpoint when the feature is active.
- *
- * When the feature is off this is a no-op. Classes in the public
- * Automattic\WooCommerce\Api\ namespace remain autoloadable — extensions
- * that want to know whether the feature is active should check
- * FeaturesUtil::feature_is_enabled( 'dual_code_graphql_api' ) rather
- * than class_exists() on the Api namespace.
- *
- * The feature-enabled check is deferred to the `rest_api_init` callback
- * to avoid triggering translation loading (via FeaturesController) before
- * the `init` action has fired, which would cause a
- * `_load_textdomain_just_in_time` notice on WordPress 6.7+.
- */
- public static function register(): void {
- add_action( 'rest_api_init', array( self::class, 'handle_rest_api_init_for_core' ) );
-
- $settings = wc_get_container()->get( Settings::class );
- $settings->register();
-
- add_action( OpcacheFileExpiry::ACTION_HOOK, array( OpcacheFileExpiry::class, 'handle_cleanup_action' ) );
- }
-
- /**
- * Hook callback: register WooCommerce core's GraphQL endpoint.
- *
- * @internal
- */
- public static function handle_rest_api_init_for_core(): void {
- if ( ! self::is_enabled() ) {
- return;
- }
-
- wc_get_container()->get( Autogenerated\GraphQLController::class )->register();
- }
-
- /**
- * Instantiate a GraphQL controller subclass and wire up its dependencies.
- *
- * Intended for sibling WooCommerce plugins that ship their own
- * autogenerated GraphQLController subclass (emitted by build-api.php
- * into their own autogenerated namespace). The returned controller is
- * ready to have handle_request() attached to a REST route.
- *
- * Returns null when the feature flag is off or PHP is < 8.1, so callers
- * can invoke this unconditionally from inside their own rest_api_init
- * handler.
- *
- * @param string $controller_class_name Fully-qualified name of a subclass of GraphQLControllerBase.
- *
- * @throws \InvalidArgumentException If $controller_class_name does not extend GraphQLControllerBase.
- */
- public static function instantiate_graphql_controller( string $controller_class_name ): ?GraphQLControllerBase {
- if ( ! self::is_enabled() ) {
- return null;
- }
-
- self::assert_is_controller_subclass( $controller_class_name );
-
- $controller = new $controller_class_name();
- $controller->init( wc_get_container()->get( QueryCache::class ) );
- return $controller;
- }
-
- /**
- * Register a GraphQL REST endpoint backed by a plugin-provided controller subclass.
- *
- * May be called at any time up to and including the `rest_api_init` hook:
- * if called earlier, registration is deferred to that hook; if called from
- * inside another plugin's `rest_api_init` handler, registration happens
- * immediately. Calls made after `rest_api_init` has already completed
- * register a REST route that WP_REST_Server won't honour on the current
- * request — callers should avoid deferring registration past bootstrap.
- *
- * When the feature flag is off or PHP is < 8.1 this is a silent no-op.
- *
- * The first argument accepts either of two forms:
- *
- * - A plugin root directory (recommended): pass `__DIR__` from the plugin's
- * bootstrap file. The controller class is resolved by convention from
- * `{dir}/src/Internal/Api/Autogenerated/GraphQLController.php` — ApiBuilder
- * always emits the generated subclass at that path.
- * - A controller class FQCN: use this when the plugin keeps its generated
- * code somewhere other than the conventional location, or registers
- * multiple endpoints backed by different controller classes.
- *
- * Plugins calling this method should guard the call with method_exists():
- *
- * if ( method_exists( \Automattic\WooCommerce\Api\Infrastructure\Main::class, 'register_graphql_endpoint' ) ) {
- * \Automattic\WooCommerce\Api\Infrastructure\Main::register_graphql_endpoint(
- * __DIR__,
- * 'my-plugin',
- * '/graphql'
- * );
- * }
- *
- * @param string $plugin_dir_or_controller_class Plugin root directory, OR a fully-qualified GraphQLController subclass name. See above.
- * @param string $route_namespace REST route namespace, as passed to register_rest_route().
- * @param string $route REST route path, as passed to register_rest_route().
- * @param string[] $methods HTTP methods accepted on the endpoint. Defaults to GET + POST.
- *
- * @throws \InvalidArgumentException If no controller can be resolved from a directory argument, or if the resolved class does not extend GraphQLControllerBase.
- */
- public static function register_graphql_endpoint(
- string $plugin_dir_or_controller_class,
- string $route_namespace,
- string $route,
- array $methods = array( 'GET', 'POST' )
- ): void {
- if ( ! self::is_enabled() ) {
- return;
- }
-
- $controller_class_name = self::resolve_controller_class( $plugin_dir_or_controller_class );
-
- // Validate up front so a typo surfaces here at bootstrap rather than
- // deep inside the deferred rest_api_init callback.
- self::assert_is_controller_subclass( $controller_class_name );
-
- $registrar = new GraphQLEndpointRegistrar( $controller_class_name, $route_namespace, $route, $methods );
-
- if ( did_action( 'rest_api_init' ) ) {
- $registrar->handle_rest_api_init();
- } else {
- add_action( 'rest_api_init', array( $registrar, 'handle_rest_api_init' ) );
- }
- }
-
- /**
- * Resolve the first argument of {@see self::register_graphql_endpoint()} to a
- * controller class name. If the argument is an existing directory, treat it
- * as the plugin root and read the namespace from the generated controller
- * file at the conventional path. Otherwise return the argument unchanged so
- * it's used as a class FQCN.
- *
- * @param string $arg Either a plugin root directory or a controller class FQCN.
- *
- * @throws \InvalidArgumentException If the argument is a directory but the
- * generated controller file is missing or
- * doesn't declare a PHP namespace.
- */
- private static function resolve_controller_class( string $arg ): string {
- if ( ! is_dir( $arg ) ) {
- return $arg;
- }
-
- $controller_file = rtrim( $arg, '/\\' ) . '/src/Internal/Api/Autogenerated/GraphQLController.php';
- if ( ! is_file( $controller_file ) ) {
- throw new \InvalidArgumentException(
- sprintf(
- 'Expected a generated GraphQL controller at %s, but the file does not exist. Run the plugin\'s API build script first, or pass an explicit controller class name.',
- esc_html( $controller_file )
- )
- );
- }
-
- // The generated controller always declares its namespace near the top
- // of the file; reading the first few KB is more than enough. Reading a
- // local file — not a URL — so wp_remote_get() does not apply here.
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
- $head = file_get_contents( $controller_file, false, null, 0, 4096 );
- if ( false === $head ) {
- throw new \InvalidArgumentException(
- sprintf( 'Could not read the controller file at %s.', esc_html( $controller_file ) )
- );
- }
-
- $namespace = self::extract_namespace_from_php_source( $head );
- if ( null === $namespace ) {
- throw new \InvalidArgumentException(
- sprintf( 'Could not determine the PHP namespace of the controller at %s.', esc_html( $controller_file ) )
- );
- }
-
- return $namespace . '\\GraphQLController';
- }
-
- /**
- * Extract an unbracketed `namespace …;` declaration from a PHP source fragment.
- *
- * Uses PHP's tokenizer rather than a regex so the parse isn't fooled by
- * declarations inside heredocs, comments, or bracketed-namespace syntax,
- * and continues to work if the generator's output format changes
- * (e.g. attributes before the declaration, single-line `<?php namespace`).
- *
- * @param string $source PHP source code.
- * @return ?string The namespace FQN without leading or trailing separator, or null if none found.
- */
- private static function extract_namespace_from_php_source( string $source ): ?string {
- $tokens = token_get_all( $source );
- $count = count( $tokens );
- for ( $i = 0; $i < $count; $i++ ) {
- if ( ! is_array( $tokens[ $i ] ) || T_NAMESPACE !== $tokens[ $i ][0] ) {
- continue;
- }
- $namespace = '';
- for ( $j = $i + 1; $j < $count; $j++ ) {
- $token = $tokens[ $j ];
- if ( is_array( $token ) ) {
- // T_NAME_QUALIFIED and T_NAME_FULLY_QUALIFIED exist on PHP 8+ and already
- // contain the full namespace path; T_STRING / T_NS_SEPARATOR are the
- // equivalent pieces on older versions.
- if ( in_array( $token[0], array( T_STRING, T_NS_SEPARATOR ), true )
- || ( defined( 'T_NAME_QUALIFIED' ) && T_NAME_QUALIFIED === $token[0] )
- || ( defined( 'T_NAME_FULLY_QUALIFIED' ) && T_NAME_FULLY_QUALIFIED === $token[0] ) ) {
- $namespace .= $token[1];
- continue;
- }
- if ( T_WHITESPACE === $token[0] ) {
- continue;
- }
- }
- // Single-character tokens `;` (unbracketed namespace) and `{` (bracketed) end the declaration.
- break;
- }
- $namespace = trim( $namespace, '\\' );
- if ( '' !== $namespace ) {
- return $namespace;
- }
- }
- return null;
- }
-
- /**
- * Assert that a class name resolves to a concrete GraphQLControllerBase subclass.
- *
- * @param string $controller_class_name Fully-qualified name of the class to validate.
- *
- * @throws \InvalidArgumentException If the class cannot be autoloaded, or does not extend GraphQLControllerBase.
- */
- private static function assert_is_controller_subclass( string $controller_class_name ): void {
- // Differentiate "class does not exist" (typo / stale autoloader) from
- // "class exists but is not a subclass" — is_subclass_of() collapses
- // both into false, which is confusing when debugging a bootstrap typo.
- if ( ! class_exists( $controller_class_name ) ) {
- throw new \InvalidArgumentException(
- sprintf(
- 'GraphQL controller class "%s" does not exist or is not autoloadable. Check the spelling, or run `composer dump-autoload` if it was added since the last autoloader regeneration.',
- esc_html( $controller_class_name )
- )
- );
- }
- if ( ! is_subclass_of( $controller_class_name, GraphQLControllerBase::class ) ) {
- throw new \InvalidArgumentException(
- sprintf(
- 'Class "%s" must extend %s.',
- esc_html( $controller_class_name ),
- esc_html( GraphQLControllerBase::class )
- )
- );
- }
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/MetadataController.php b/plugins/woocommerce/src/Api/Infrastructure/MetadataController.php
deleted file mode 100644
index 960ab3f5c2a..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/MetadataController.php
+++ /dev/null
@@ -1,396 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Error;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-use Automattic\WooCommerce\Api\Utils\SchemaHandle;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-
-/**
- * Hand-written controller that contributes the `_apiMetadata` root query
- * field and the supporting `MetadataEntry`, `MetadataTarget`, `MetadataValue`
- * and `AuthEntry` types to the generated schema.
- *
- * The autogenerated `RootQueryType` references this controller alongside the
- * autogenerated query resolvers, so the field appears on the root `Query`
- * type without any special wiring at the controller level. The resolver
- * delegates to {@see SchemaHandle::find_metadata()} for the schema walk and
- * filter application, then reshapes the rows so each entry is exposed as the
- * `{ name, value }` pair that `MetadataEntry` expects. Authorization
- * descriptors (each row's `authorization` list) pass through unchanged.
- *
- * Access is gated by {@see self::can_query_metadata()}; once allowed, the
- * returned content is principal-independent — the full declared shape of the
- * schema, irrespective of who is calling.
- */
-class MetadataController {
- /**
- * Memoised `MetadataValue` scalar type.
- *
- * @var ?CustomScalarType
- */
- private static ?CustomScalarType $value_scalar = null;
-
- /**
- * Memoised `MetadataEntry` output type.
- *
- * @var ?ObjectType
- */
- private static ?ObjectType $entry_type = null;
-
- /**
- * Memoised `MetadataTarget` output type.
- *
- * @var ?ObjectType
- */
- private static ?ObjectType $target_type = null;
-
- /**
- * Memoised `AuthEntry` output type — describes one authorization
- * attribute attached to a schema target.
- *
- * @var ?ObjectType
- */
- private static ?ObjectType $auth_entry_type = null;
-
- /**
- * GraphQL field name used on the root `Query` type.
- */
- public const FIELD_NAME = '_apiMetadata';
-
- /**
- * Field definition for the root `_apiMetadata` query, in the shape the
- * autogenerated `RootQueryType` expects (same as every autogenerated
- * resolver's `get_field_definition()`).
- *
- * @return array<string, mixed>
- */
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_target_type() ) ) ),
- 'description' => __(
- 'Lists metadata attached to elements of this schema. All filter arguments are optional; supplying multiple narrows the result. Use this to discover internal-use APIs, beta features, ownership, etc., or to ask "can I use this specific element?".',
- 'woocommerce'
- ),
- 'args' => array(
- 'name' => array(
- 'type' => Type::string(),
- 'description' => __( 'Match rows that carry a metadata entry with this name. Surviving rows have their entries trimmed to the matching one.', 'woocommerce' ),
- ),
- 'type' => array(
- 'type' => Type::string(),
- 'description' => __( 'Match rows whose target type equals this name.', 'woocommerce' ),
- ),
- 'field' => array(
- 'type' => Type::string(),
- 'description' => __( 'Match rows whose target field equals this name.', 'woocommerce' ),
- ),
- 'attribute' => array(
- 'type' => Type::string(),
- 'description' => __( 'Match rows whose authorization carries an attribute with this class short name. Surviving rows have their authorization trimmed to the matching descriptors.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- /**
- * Resolver for the `_apiMetadata` root field. Signature matches the
- * engine's resolver contract; `$root` is unused here (root operations
- * have no parent). `$context` is read for the principal so the
- * `can_query_metadata` ladder can run.
- *
- * @param ?array $root The engine passes null for root resolvers.
- * @param array $args GraphQL arguments (`name`, `type`, `field`, `attribute`).
- * @param mixed $context Per-request context — an ArrayObject wrapping {`principal`, `_query_metadata`}.
- * @param ResolveInfo $info Carries the schema instance to walk.
- * @return list<array<string, mixed>>
- * @throws Error When the principal is not allowed to query `_apiMetadata`.
- */
- public static function resolve( ?array $root, array $args, mixed $context, ResolveInfo $info ): array {
- unset( $root );
-
- $principal = is_object( $context ) || is_array( $context ) ? ( $context['principal'] ?? null ) : null;
- if ( ! self::can_query_metadata( $principal ) ) {
- // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Static error message + machine code; serialized as JSON, not HTML.
- throw self::build_metadata_query_authorization_error( $principal );
- }
-
- // Wrap the resolver's engine-typed schema into the same handle clients
- // receive from `GraphQLControllerBase::get_schema()`, so the resolver and
- // PHP-side callers share a single inspection surface.
- $schema = new SchemaHandle( $info->schema );
-
- $rows = $schema->find_metadata(
- $args['name'] ?? null,
- $args['type'] ?? null,
- $args['field'] ?? null,
- $args['attribute'] ?? null,
- );
-
- // SchemaHandle returns entries as an associative `name => value` map,
- // which is the natural shape for filtering and PHP-side consumers. The
- // GraphQL `MetadataEntry` type instead exposes each entry as a
- // `{ name, value }` object so clients can `entries { name value }` over
- // a list. Reshape here.
- return array_map(
- static function ( array $row ): array {
- $row['entries'] = array_map(
- static fn( string $entry_name, $entry_value ): array => array(
- 'name' => $entry_name,
- 'value' => $entry_value,
- ),
- array_keys( $row['entries'] ),
- array_values( $row['entries'] ),
- );
- return $row;
- },
- $rows
- );
- }
-
- /**
- * Whether the principal may run the `_apiMetadata` query.
- *
- * Tri-tier ladder, deliberately fail-closed:
- *
- * 1. If the principal declares `can_query_metadata(): bool`, use it.
- * Plugins distinguish metadata-query access from native
- * introspection access by declaring this method.
- * 2. Else if the principal declares `can_introspect(): bool`, fall
- * back to it — one switch then gates both metadata and
- * introspection, which is the common case.
- * 3. Else (neither method declared) deny. Plugin authors that don't
- * opt their principal in get a locked-down endpoint rather than
- * leaking schema shape and gate descriptors by default.
- *
- * The principal-derived decision is then passed through the
- * {@see 'woocommerce_graphql_can_query_metadata'} filter so sites
- * can grant or revoke access without subclassing the principal —
- * useful for per-request rules (specific IPs, headers, query
- * parameters, etc.).
- *
- * Fail-closed contract: null principal denies before the filter is
- * consulted; either method's return is checked with `=== true`; any
- * throw from the principal method or the filter denies; the filter
- * must likewise return strictly `true` to allow.
- *
- * @param ?object $principal The resolved principal, or null when principal resolution failed.
- */
- private static function can_query_metadata( ?object $principal ): bool {
- if ( null === $principal ) {
- return false;
- }
-
- try {
- if ( method_exists( $principal, 'can_query_metadata' ) ) {
- $allowed = true === $principal->can_query_metadata();
- } elseif ( method_exists( $principal, 'can_introspect' ) ) {
- $allowed = true === $principal->can_introspect();
- } else {
- $allowed = false;
- }
-
- /**
- * Filters whether the current principal may run the `_apiMetadata` query.
- *
- * The filter receives the principal-derived decision (see the tri-tier
- * ladder in {@see MetadataController::can_query_metadata()}) and must
- * return strictly `true` to grant access; any other return value
- * denies. The filter is not invoked when principal resolution failed
- * (i.e. when the resolver receives a null principal) — that case
- * denies outright.
- *
- * @since 10.9.0
- *
- * @internal
- *
- * @param bool $allowed Whether the principal may query `_apiMetadata`.
- * @param object $principal The resolved principal.
- */
- $allowed = apply_filters( 'woocommerce_graphql_can_query_metadata', $allowed, $principal );
- } catch ( \Throwable $e ) {
- return false;
- }
-
- return true === $allowed;
- }
-
- /**
- * Build the GraphQL error thrown when `_apiMetadata` is queried by a
- * principal that cannot. Mirrors
- * {@see ResolverHelpers::build_authorization_error()}'s
- * UNAUTHORIZED / FORBIDDEN distinction so clients can branch on
- * `extensions.code` the same way they do for field-level denies.
- *
- * @param ?object $principal The resolved principal (null when principal resolution failed).
- */
- private static function build_metadata_query_authorization_error( ?object $principal ): Error {
- $is_anonymous = null === $principal
- || ( method_exists( $principal, 'is_authenticated' ) && ! $principal->is_authenticated() );
- return new Error(
- $is_anonymous ? 'Authentication required.' : 'You do not have permission to perform this action.',
- extensions: array( 'code' => $is_anonymous ? 'UNAUTHORIZED' : 'FORBIDDEN' )
- );
- }
-
- /**
- * The `MetadataTarget` output type, lazily built and cached.
- */
- private static function get_target_type(): ObjectType {
- if ( null === self::$target_type ) {
- self::$target_type = new ObjectType(
- array(
- 'name' => 'MetadataTarget',
- 'description' => __(
- 'One element of the schema with its attached metadata. Type-level rows have `field`, `argument` and `enumValue` set to null; field-level rows set `field` (and `argument` when the target is a field argument); enum-value rows set `enumValue`.',
- 'woocommerce'
- ),
- 'fields' => fn() => array(
- 'type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'Name of the GraphQL type this row describes.', 'woocommerce' ),
- ),
- 'field' => array(
- 'type' => Type::string(),
- 'description' => __( 'Field name when this row describes a field (or a field argument); null for type-level rows.', 'woocommerce' ),
- ),
- 'argument' => array(
- 'type' => Type::string(),
- 'description' => __( 'Argument name when this row describes a field argument; null otherwise.', 'woocommerce' ),
- ),
- 'enumValue' => array(
- 'type' => Type::string(),
- 'description' => __( 'Enum value name when this row describes one specific enum value; null otherwise.', 'woocommerce' ),
- ),
- 'entries' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_entry_type() ) ) ),
- 'description' => __( 'Metadata entries attached to the target.', 'woocommerce' ),
- ),
- 'authorization' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( self::get_auth_entry_type() ) ) ),
- 'description' => __( 'Authorization attributes attached to the target (e.g. `RequiredCapability`, `PublicAccess`, or plugin-defined). Empty when the target carries no authorization attributes.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$target_type;
- }
-
- /**
- * The `AuthEntry` output type — one authorization attribute attached
- * to a target. Carries the attribute's short class name and the
- * scalar args supplied at the usage site.
- */
- private static function get_auth_entry_type(): ObjectType {
- if ( null === self::$auth_entry_type ) {
- self::$auth_entry_type = new ObjectType(
- array(
- 'name' => 'AuthEntry',
- 'description' => __( 'One authorization attribute attached to a schema target.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'attribute' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'Short class name of the authorization attribute (e.g. `RequiredCapability`).', 'woocommerce' ),
- ),
- 'args' => array(
- 'type' => Type::nonNull( Type::listOf( self::get_value_scalar() ) ),
- 'description' => __( 'Constructor arguments supplied at the usage site, in source order. Element type is the same scalar union as `MetadataValue`.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$auth_entry_type;
- }
-
- /**
- * The `MetadataEntry` output type, lazily built and cached.
- */
- private static function get_entry_type(): ObjectType {
- if ( null === self::$entry_type ) {
- self::$entry_type = new ObjectType(
- array(
- 'name' => 'MetadataEntry',
- 'description' => __( 'One metadata entry: a `name` plus a scalar `value`.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'Identifier of the entry (e.g. `internal`, `beta`).', 'woocommerce' ),
- ),
- 'value' => array(
- // Nullable: `MetadataValue` itself permits a null payload (e.g.
- // `#[Metadata( 'deprecated_reason', null )]`), so the wrapping
- // must allow it through.
- 'type' => self::get_value_scalar(),
- 'description' => __( 'Scalar payload associated with the entry. Null when the metadata entry carries a null value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$entry_type;
- }
-
- /**
- * The `MetadataValue` custom scalar, accepting any GraphQL-compatible scalar.
- *
- * The autogenerated scalar template hard-codes acceptance of string
- * literals only, so this scalar is hand-built rather than going through
- * ApiBuilder. `parseLiteral` walks the AST node types and `parseValue`
- * accepts the already-decoded PHP scalar that variables-mode delivers.
- */
- private static function get_value_scalar(): CustomScalarType {
- if ( null === self::$value_scalar ) {
- self::$value_scalar = new CustomScalarType(
- array(
- 'name' => 'MetadataValue',
- 'description' => __(
- 'Scalar payload of a metadata entry. Accepts a string, integer, float, boolean, or null.',
- 'woocommerce'
- ),
- // Resolvers return the raw PHP scalar; webonyx serialises it as JSON directly.
- 'serialize' => static fn( $value ) => $value,
- 'parseValue' => static function ( $value ) {
- if ( null === $value || is_bool( $value ) || is_int( $value ) || is_float( $value ) || is_string( $value ) ) {
- return $value;
- }
- throw new Error( 'MetadataValue must be a string, integer, float, boolean, or null.' );
- },
- 'parseLiteral' => static function ( $value_node, ?array $variables = null ) {
- unset( $variables );
-
- if ( $value_node instanceof StringValueNode ) {
- return $value_node->value;
- }
- if ( $value_node instanceof BooleanValueNode ) {
- return $value_node->value;
- }
- if ( $value_node instanceof IntValueNode ) {
- return (int) $value_node->value;
- }
- if ( $value_node instanceof FloatValueNode ) {
- return (float) $value_node->value;
- }
- if ( $value_node instanceof NullValueNode ) {
- return null;
- }
- throw new Error( 'MetadataValue must be a string, integer, float, boolean, or null literal.' );
- },
- )
- );
- }
- return self::$value_scalar;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Principal.php b/plugins/woocommerce/src/Api/Infrastructure/Principal.php
deleted file mode 100644
index 448655271f5..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Principal.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-/**
- * Default principal class for the WooCommerce dual code+GraphQL API.
- *
- * Plugins that authenticate against something other than WordPress users must ship
- * their own principal class at `<plugin-api-namespace>\Infrastructure\...` together
- * with a matching `PrincipalResolver`. Plugins that build on WP-user auth can either
- * use this class directly (no resolver needed; the controller falls back to
- * `new Principal( wp_get_current_user() )`) or extend it to add their own
- * fields.
- */
-class Principal {
- /**
- * Constructor.
- *
- * @param \WP_User $user The WordPress user behind the request. For anonymous requests this is a `WP_User` with `ID === 0`, as returned by {@see \wp_get_current_user()}.
- */
- public function __construct(
- public readonly \WP_User $user,
- ) {
- }
-
- /**
- * Whether the underlying WP user is authenticated.
- *
- * Convenience for `$principal->user->ID > 0`, the canonical anonymous
- * marker in WordPress. Use this in `authorize()` / `execute()` bodies that
- * need to distinguish anonymous from authenticated callers.
- */
- public function is_authenticated(): bool {
- return $this->user->ID > 0;
- }
-
- /**
- * Whether this principal may run GraphQL schema introspection on the endpoint.
- *
- * Implementing `can_introspect()` is opt-in for plugin principal classes,
- * a principal that doesn't define it is denied by default. Plugins building
- * authenticated endpoints should make an explicit decision per principal
- * model rather than inheriting an introspection policy by accident.
- */
- public function can_introspect(): bool {
- return user_can( $this->user, 'manage_woocommerce' );
- }
-
- /**
- * Whether this principal may activate GraphQL debug mode on the endpoint.
- *
- * Implementing `can_use_debug_mode()` is opt-in for plugin principal classes,
- * a principal that doesn't define it is denied by default. Plugins building
- * authenticated endpoints should make an explicit decision per principal
- * model rather than inheriting a debug mode policy by accident.
- *
- * Note that this method's outcome is necessary but not sufficient for debug
- * mode to be active: the controller also requires the request to carry
- * `_debug=1`. The decision can be overridden by the
- * `woocommerce_graphql_can_use_debug_mode` filter.
- */
- public function can_use_debug_mode(): bool {
- return user_can( $this->user, 'manage_options' );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/PrincipalResolver.php b/plugins/woocommerce/src/Api/Infrastructure/PrincipalResolver.php
deleted file mode 100644
index 8353002f1c0..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/PrincipalResolver.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-/**
- * Default principal resolver for the WooCommerce dual code+GraphQL API.
- *
- * Plugins that implement their own API and authenticate against something
- * other than WordPress users (e.g. an app token) must ship their own resolver
- * at `<plugin-api-namespace>\Infrastructure\PrincipalResolver` with a
- * `resolve_principal( \WP_REST_Request ): T` (or zero-arg) method whose
- * return type is the plugin's own principal class. ApiBuilder detects it
- * during generation and routes the autogenerated controller through it.
- *
- * The \WP_REST_Request parameter is optional; the default resolver doesn't
- * inspect headers (WordPress's auth pipeline has already populated the global
- * current user by the time this fires).
- */
-final class PrincipalResolver {
- /**
- * Resolve the request principal.
- *
- * Anonymous requests are signalled by a Principal whose underlying
- * `WP_User` has `ID === 0` (see {@see Principal::is_authenticated()}).
- */
- public function resolve_principal(): Principal {
- return new Principal( wp_get_current_user() );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/QueryInfoExtractor.php b/plugins/woocommerce/src/Api/Infrastructure/QueryInfoExtractor.php
deleted file mode 100644
index 92f6b0e3133..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/QueryInfoExtractor.php
+++ /dev/null
@@ -1,241 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-
-/**
- * Extracts a unified query info tree from a GraphQL ResolveInfo.
- *
- * The resulting array captures the full query structure: fields, arguments,
- * sub-selections, inline fragments, and named fragment spreads.
- *
- * Structure rules:
- * - Leaf field (no args, no sub-selection) => true
- * - Field with sub-selections => nested associative array
- * - Field arguments => '__args' reserved key
- * - Inline fragments with a type condition => '...TypeName' prefix key
- * - Inline fragments without a type condition and named fragment spreads =>
- * expanded inline (merged into the parent as siblings of the other
- * selections), matching how GraphQL evaluates them
- * - Top-level query args included via '__args'
- */
-class QueryInfoExtractor {
- /**
- * Extract query info from a resolver's ResolveInfo and top-level args.
- *
- * @param ResolveInfo $info The GraphQL resolve info.
- * @param array $args The top-level query arguments.
- * @return array The unified query info tree.
- */
- public static function extract_from_info( ResolveInfo $info, array $args ): array {
- $result = self::extract( $info->fieldNodes[0]->selectionSet ?? null, $info->variableValues, $info->fragments );
- if ( ! empty( $args ) ) {
- $result['__args'] = $args;
- }
- return $result;
- }
-
- /**
- * Recursively extract query info from a selection set.
- *
- * @internal Recursive helper exposed only for internal callers and tests;
- * the engine-decoupled entry point for autogenerated resolvers
- * is {@see self::extract_from_info()}.
- *
- * @param ?SelectionSetNode $selection_set The selection set to process.
- * @param array $variable_values Variable values for resolving arguments.
- * @param array<string, FragmentDefinitionNode> $fragments Named fragment definitions from the document.
- * @return array The query info tree for the selection set.
- */
- public static function extract( ?SelectionSetNode $selection_set, array $variable_values, array $fragments = array() ): array {
- $expanded_fragments = array();
-
- return self::extract_selection_set( $selection_set, $variable_values, $fragments, $expanded_fragments );
- }
-
- /**
- * Recursive worker behind {@see self::extract()}.
- *
- * Named fragments are expanded once per extract() call and the result is
- * reused for every further spread, so the work stays proportional to the
- * size of the document. This runs after validation, whose limits don't
- * bound how often a fragment is spread.
- *
- * @param ?SelectionSetNode $selection_set The selection set to process.
- * @param array $variable_values Variable values for resolving arguments.
- * @param array<string, FragmentDefinitionNode> $fragments Named fragment definitions from the document.
- * @param array<string, array> $expanded_fragments Memoized expansions, keyed by fragment name. Passed by reference so the whole walk shares one cache.
- * @return array The query info tree for the selection set.
- */
- private static function extract_selection_set( ?SelectionSetNode $selection_set, array $variable_values, array $fragments, array &$expanded_fragments ): array {
- if ( null === $selection_set ) {
- return array();
- }
-
- $result = array();
-
- foreach ( $selection_set->selections as $selection ) {
- if ( $selection instanceof FieldNode ) {
- $field_name = $selection->name->value;
- $result[ $field_name ] = self::build_field_entry( $selection, $variable_values, $fragments, $expanded_fragments );
- } elseif ( $selection instanceof InlineFragmentNode ) {
- $sub = self::extract_selection_set( $selection->selectionSet, $variable_values, $fragments, $expanded_fragments );
- if ( null === $selection->typeCondition ) {
- // No `on Type` clause (e.g. `... @include(if: $flag) { ... }`):
- // the fragment applies to the parent type, so merge it like
- // a named fragment spread.
- $result = self::merge_selections( $result, $sub );
- } else {
- $result[ '...' . $selection->typeCondition->name->value ] = $sub;
- }
- } elseif ( $selection instanceof FragmentSpreadNode ) {
- // Expand named fragment spreads inline: their fields become
- // siblings of the other selections, matching how GraphQL
- // evaluates them. Consumers of _query_info (mappers that
- // check array_key_exists for specific fields) see them the
- // same as if the fragment had been written inline. Use a
- // recursive merge so overlapping selections are unioned
- // rather than replaced — `array_merge` would drop the
- // existing sub-selection under the same field name.
- $spread = self::expand_fragment( $selection->name->value, $variable_values, $fragments, $expanded_fragments );
- if ( null === $spread ) {
- continue;
- }
- $result = self::merge_selections( $result, $spread );
- }
- }
-
- return $result;
- }
-
- /**
- * Expand a named fragment into its query info tree, memoizing the result.
- *
- * @param string $name The fragment name.
- * @param array $variable_values Variable values for resolving arguments.
- * @param array<string, FragmentDefinitionNode> $fragments Named fragment definitions from the document.
- * @param array<string, array> $expanded_fragments Memoized expansions, keyed by fragment name.
- * @return ?array The expanded tree, or null when the fragment is not defined.
- */
- private static function expand_fragment( string $name, array $variable_values, array $fragments, array &$expanded_fragments ): ?array {
- if ( array_key_exists( $name, $expanded_fragments ) ) {
- return $expanded_fragments[ $name ];
- }
-
- $fragment = $fragments[ $name ] ?? null;
- if ( null === $fragment ) {
- return null;
- }
-
- // Seed the entry before recursing so a fragment cycle expands to nothing
- // instead of recursing forever (defensive: NoFragmentCycles rejects
- // such documents during validation).
- $expanded_fragments[ $name ] = array();
- $expanded_fragments[ $name ] = self::extract_selection_set( $fragment->selectionSet, $variable_values, $fragments, $expanded_fragments );
-
- return $expanded_fragments[ $name ];
- }
-
- /**
- * Build the entry for a single field node.
- *
- * @param FieldNode $field The field node.
- * @param array $variable_values Variable values for resolving arguments.
- * @param array<string, FragmentDefinitionNode> $fragments Named fragment definitions from the document.
- * @param array<string, array> $expanded_fragments Memoized fragment expansions, keyed by fragment name.
- * @return array|bool True for leaf fields, associative array otherwise.
- */
- private static function build_field_entry( FieldNode $field, array $variable_values, array $fragments, array &$expanded_fragments ): array|bool {
- $has_args = ! empty( $field->arguments ) && count( $field->arguments ) > 0;
- $has_sub_selection = null !== $field->selectionSet;
-
- if ( ! $has_args && ! $has_sub_selection ) {
- return true;
- }
-
- $entry = array();
-
- if ( $has_args ) {
- $args = array();
- foreach ( $field->arguments as $arg ) {
- $args[ $arg->name->value ] = self::resolve_argument_value( $arg, $variable_values );
- }
- $entry['__args'] = $args;
- }
-
- if ( $has_sub_selection ) {
- $sub = self::extract_selection_set( $field->selectionSet, $variable_values, $fragments, $expanded_fragments );
- $entry = self::merge_selections( $entry, $sub );
- }
-
- return $entry;
- }
-
- /**
- * Recursively merge two selection trees produced by extract()/build_field_entry().
- *
- * Used wherever selections from different sources are combined under
- * the same key (notably: named fragment spreads expanded inline). Matches
- * GraphQL's selection-set merge semantics — overlapping fields have their
- * sub-selections unioned rather than one replacing the other, which a
- * shallow `array_merge` would do.
- *
- * Rules:
- * - Key only in one side: kept verbatim.
- * - Both sides arrays: recurse, unioning children.
- * - One array, one `true` (leaf): keep the array — it carries the
- * sub-selection detail, and its presence already implies the field
- * was requested.
- * - Both `true`: keep `true`.
- * - `__args` collisions (same field with different argument values):
- * the second operand wins. Conflicting field args are a GraphQL
- * validation error upstream of us, so this path is defensive.
- *
- * @param array $a First selection tree.
- * @param array $b Second selection tree, merged into $a.
- * @return array The merged tree.
- */
- private static function merge_selections( array $a, array $b ): array {
- foreach ( $b as $key => $value ) {
- if ( ! array_key_exists( $key, $a ) ) {
- $a[ $key ] = $value;
- continue;
- }
- $existing = $a[ $key ];
- if ( is_array( $existing ) && is_array( $value ) ) {
- $a[ $key ] = self::merge_selections( $existing, $value );
- } elseif ( is_array( $value ) ) {
- // One side is `true`, the other is a sub-selection array — keep the array.
- $a[ $key ] = $value;
- }
- // Both true, or existing-array + new-true: keep existing.
- }
- return $a;
- }
-
- /**
- * Resolve the value of a single argument node, handling variables.
- *
- * @param ArgumentNode $arg The argument node.
- * @param array $variable_values Variable values.
- * @return mixed The resolved argument value.
- */
- private static function resolve_argument_value( ArgumentNode $arg, array $variable_values ): mixed {
- $value_node = $arg->value;
-
- if ( $value_node instanceof \Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode ) {
- return $variable_values[ $value_node->name->value ] ?? null;
- }
-
- return \Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::valueFromASTUntyped( $value_node, $variable_values );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/ResolverHelpers.php b/plugins/woocommerce/src/Api/Infrastructure/ResolverHelpers.php
deleted file mode 100644
index 8516cae5c3c..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/ResolverHelpers.php
+++ /dev/null
@@ -1,481 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Error;
-
-/**
- * Shared utilities for the auto-generated GraphQL resolvers.
- *
- * The public surface uses only {@see Schema\Error} (a stable subclass of the
- * engine's Error) on throws/returns so generated code never imports an
- * engine-specific symbol — a future engine switch can rewrite the bodies
- * here without invalidating already-committed plugin trees.
- */
-class ResolverHelpers {
- /**
- * Compute the complexity cost of a paginated connection field.
- *
- * Used as the `complexity` callable on every generated resolver field
- * that returns a `Connection`. Runs during query validation (before
- * resolver execution, so before `PaginationParams::validate_args()` has
- * a chance to reject bad input) — so out-of-range / wrong-type values
- * are clamped to MAX_PAGE_SIZE here. Using MAX_PAGE_SIZE as the
- * fallback means a malicious attempt to shrink cost via e.g. a
- * negative `first` value only inflates the computed complexity,
- * closing the cost-bypass angle.
- *
- * @param int $child_complexity The complexity of a single child node.
- * @param array $args The field arguments (expects `first` / `last`).
- *
- * @return int The total complexity for this connection field.
- */
- public static function complexity_from_pagination( int $child_complexity, array $args ): int {
- $requested = $args['first'] ?? $args['last'] ?? \Automattic\WooCommerce\Api\Pagination\PaginationParams::get_default_page_size();
- $page_size = ( is_int( $requested ) && $requested >= 0 && $requested <= \Automattic\WooCommerce\Api\Pagination\PaginationParams::MAX_PAGE_SIZE )
- ? $requested
- : \Automattic\WooCommerce\Api\Pagination\PaginationParams::MAX_PAGE_SIZE;
- return $page_size * ( $child_complexity + 1 );
- }
-
- /**
- * Build a PaginationParams instance from the standard GraphQL pagination
- * arguments (first, last, after, before).
- *
- * @param array $args The GraphQL field arguments.
- *
- * @return \Automattic\WooCommerce\Api\Pagination\PaginationParams
- * @throws Error When a pagination value is out of range.
- */
- public static function create_pagination_params( array $args ): \Automattic\WooCommerce\Api\Pagination\PaginationParams {
- return self::create_input(
- fn() => new \Automattic\WooCommerce\Api\Pagination\PaginationParams(
- first: $args['first'] ?? null,
- last: $args['last'] ?? null,
- after: $args['after'] ?? null,
- before: $args['before'] ?? null,
- )
- );
- }
-
- /**
- * Invoke a factory callable, catching InvalidArgumentException and
- * converting it to a client-visible GraphQL error.
- *
- * Used to wrap construction of unrolled input types (PaginationParams,
- * ProductFilterInput, etc.) whose constructors may validate their
- * arguments and throw.
- *
- * @param callable $factory A callable that returns the constructed object.
- *
- * @return mixed The return value of the factory.
- * @throws Error When the factory throws InvalidArgumentException.
- */
- public static function create_input( callable $factory ): mixed {
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- try {
- return $factory();
- } catch ( \InvalidArgumentException $e ) {
- throw new Error(
- $e->getMessage(),
- extensions: array( 'code' => 'INVALID_ARGUMENT' )
- );
- }
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
-
- /**
- * Execute a command's execute() method, translating any thrown exceptions
- * into spec-compliant GraphQL errors.
- *
- * @param object $command The command instance (must have an execute() method).
- * @param array $execute_args Named arguments to pass to execute().
- *
- * @return mixed The return value of execute().
- * @throws Error On any exception from the command.
- */
- public static function execute_command( object $command, array $execute_args ): mixed {
- return self::translate_exceptions(
- static fn() => $command->execute( ...$execute_args )
- );
- }
-
- /**
- * Invoke a command's authorize() method, translating any thrown exceptions
- * into spec-compliant GraphQL errors.
- *
- * Mirror of execute_command() for the authorize step. Needed because an
- * authorize() call can throw an ApiException (e.g. UnauthorizedException
- * when a target record does not exist); without this wrapper the
- * exception would propagate up to the engine and lose its error code and
- * user-visible message on its way through the generic error formatter.
- *
- * @param object $command The command instance (must have an authorize() method).
- * @param array $authorize_args Named arguments to pass to authorize().
- *
- * @return bool The return value of authorize().
- * @throws Error On any exception from the authorize method.
- */
- public static function authorize_command( object $command, array $authorize_args ): bool {
- return self::translate_exceptions(
- static fn() => $command->authorize( ...$authorize_args )
- );
- }
-
- /**
- * Build the GraphQL error to throw when an authorization check fails.
- *
- * Distinguishes the two HTTP-correct shapes:
- * - **UNAUTHORIZED (401)** when the principal is anonymous — the caller
- * could plausibly fix it by authenticating, so the response invites
- * re-auth.
- * - **FORBIDDEN (403)** otherwise — the principal is recognised but
- * isn't allowed; re-authenticating wouldn't help.
- *
- * The "anonymous" check is opt-in by convention: the principal's
- * `is_authenticated(): bool` method, when present, decides. Principals
- * that don't define it fall through to FORBIDDEN — generated resolvers
- * still emit a coded error, just without the 401/403 distinction.
- *
- * Used for class-level denials (operation-level "you cannot call this
- * query/mutation"). For field-level denials that should carry a
- * structured `subject` payload (type / field / attribute), see
- * {@see self::build_field_authorization_error()}.
- *
- * @param object $principal The resolved request principal.
- */
- public static function build_authorization_error( object $principal ): Error {
- $is_anonymous = method_exists( $principal, 'is_authenticated' ) && ! $principal->is_authenticated();
- return new Error(
- $is_anonymous ? 'Authentication required.' : 'You do not have permission to perform this action.',
- extensions: array( 'code' => $is_anonymous ? 'UNAUTHORIZED' : 'FORBIDDEN' )
- );
- }
-
- /**
- * Like {@see self::build_authorization_error()} but carries a structured
- * `subject` payload identifying *what* was denied — the enclosing type,
- * the field (when applicable), and the attribute class name driving the
- * decision. Clients can branch on `extensions.subject.field` to tell a
- * field-level deny apart from an operation-level one.
- *
- * The error code (UNAUTHORIZED / FORBIDDEN) is preserved verbatim so
- * existing client handlers continue to work; the subject payload is
- * additive.
- *
- * @param object $principal The resolved request principal.
- * @param string $type GraphQL type name carrying the gate.
- * @param ?string $field Field name when the deny is field-level; null for type/operation-level denies.
- * @param string $attribute_short Short class name of the deciding authorization attribute (no namespace).
- */
- public static function build_field_authorization_error( object $principal, string $type, ?string $field, string $attribute_short ): Error {
- $is_anonymous = method_exists( $principal, 'is_authenticated' ) && ! $principal->is_authenticated();
- $subject = array(
- 'type' => $type,
- 'attribute' => $attribute_short,
- );
- if ( null !== $field ) {
- $subject['field'] = $field;
- }
- return new Error(
- $is_anonymous ? 'Authentication required.' : 'You do not have permission to perform this action.',
- extensions: array(
- 'code' => $is_anonymous ? 'UNAUTHORIZED' : 'FORBIDDEN',
- 'subject' => $subject,
- )
- );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for the given command and
- * principal (the AND of the autodiscovered authorization attributes'
- * authorize() outcomes).
- *
- * Lets code-API callers (and tests) ask "would this command's attribute-based
- * authorization grant access to this principal?" without going through the
- * GraphQL pipeline.
- *
- * Note that it returns true when the command has no authorization attributes
- * (in that case the command's own `authorize()` method, if any, is the sole
- * guard; and consulting it requires running the command, which this helper
- * deliberately doesn't do).
- *
- * Note: this provides the attribute-level authorization only. A command with
- * both attributes and an `authorize()` method composes the two via the
- * `_preauthorized` infrastructure parameter; this helper returns the value
- * that `_preauthorized` would carry, not the final `authorize()` outcome.
- *
- * Scope is class-level (queries / mutations). Field-level authorization
- * lives on output-type / input-type properties and is enforced inside
- * the generated resolvers. To inspect a field's declared authorization
- * from code, walk {@see \Automattic\WooCommerce\Api\Utils\SchemaHandle::find_metadata()}
- * and read the `authorization` slice on each row.
- *
- * @param string $command_fqcn Fully-qualified command class name.
- * @param object $principal The resolved principal. Anonymous requests are represented by a sentinel principal (e.g. {@see \Automattic\WooCommerce\Api\Infrastructure\Principal} whose underlying WP_User has ID=0), not by null.
- *
- * @throws \InvalidArgumentException When `$command_fqcn` does not name an existing class.
- */
- public static function compute_preauthorized( string $command_fqcn, object $principal ): bool {
- if ( ! class_exists( $command_fqcn ) ) {
- throw new \InvalidArgumentException(
- sprintf( 'Class %s does not exist.', esc_html( $command_fqcn ) )
- );
- }
- $ref = new \ReflectionClass( $command_fqcn );
- $direct = self::collect_authorization_instances( $ref );
- $usages = $direct;
- if ( empty( $usages ) ) {
- // No direct attribute — collect from the entire ancestor tree:
- // the parent chain plus each ancestor's traits and interfaces
- // (recursively). All inherited sources contribute as peers; the
- // only thing direct attributes shadow is the inherited tree as a
- // whole. Mirrors
- // {@see \Automattic\WooCommerce\Api\Infrastructure\DesignTime\ApiBuilder::resolve_authorization()}.
- $visited = array();
- $stack = array_merge(
- $ref->getParentClass() ? array( $ref->getParentClass() ) : array(),
- $ref->getTraits(),
- $ref->getInterfaces(),
- );
- while ( ! empty( $stack ) ) {
- $source = array_shift( $stack );
- $name = $source->getName();
- if ( in_array( $name, $visited, true ) ) {
- continue;
- }
- $visited[] = $name;
- $usages = array_merge( $usages, self::collect_authorization_instances( $source ) );
- if ( false !== $source->getParentClass() ) {
- $stack[] = $source->getParentClass();
- }
- $stack = array_merge( $stack, $source->getTraits(), $source->getInterfaces() );
- }
- }
-
- $query_metadata = self::harvest_class_metadata( $ref );
-
- foreach ( $usages as $instance ) {
- $auth_method = new \ReflectionMethod( $instance, 'authorize' );
- $call_args = self::build_authorize_call_args(
- $auth_method,
- $principal,
- array( 'query' => $query_metadata ),
- array(),
- null
- );
- $result = $instance->authorize( ...$call_args );
- if ( ! $result ) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Mirror of `ApiBuilder::harvest_metadata()` for the runtime path. Walks
- * {@see \Automattic\WooCommerce\Api\Attributes\Metadata}-subclass attributes
- * on a class reflector and returns `name => value`. Duplicate names are
- * resolved last-wins — the build-time validator already errors on
- * duplicates, so this is only relevant for in-process classes that
- * never went through a build.
- *
- * The per-target `_apiMetadata` opt-out (`shows_in_metadata_query()`)
- * is not applied here: the `$_metadata` slot threaded into a class-
- * level attribute's `authorize()` is for policy input, not discovery,
- * so attribute authors see every entry regardless of how it surfaces
- * through `_apiMetadata`.
- *
- * @param \ReflectionClass $ref The class to read metadata from.
- * @return array<string, bool|int|float|string|null>
- */
- private static function harvest_class_metadata( \ReflectionClass $ref ): array {
- $entries = array();
- foreach ( $ref->getAttributes( \Automattic\WooCommerce\Api\Attributes\Metadata::class, \ReflectionAttribute::IS_INSTANCEOF ) as $attribute ) {
- $instance = $attribute->newInstance();
- $entries[ $instance->get_name() ] = $instance->get_value();
- }
- return $entries;
- }
-
- /**
- * Build the positional/named argument list for an attribute's `authorize()`
- * method based on which opt-in slots its signature declares.
- *
- * The principal is always passed first (positionally) when the method
- * declares a non-`_`-prefixed parameter; infrastructure parameters
- * (`$_metadata`, `$_args`, `$_parent`) are passed as named arguments so
- * the attribute can omit any subset without affecting the call shape.
- *
- * @param \ReflectionMethod $method The attribute's `authorize()` method.
- * @param object $principal The resolved principal to pass when the method takes one.
- * @param array $metadata Value for `$_metadata` (passed if the method declares it).
- * @param array $args Value for `$_args` (passed if the method declares it).
- * @param mixed $parent Value for `$_parent` (passed if the method declares it).
- *
- * @return array<int|string, mixed> Positional principal first (if any), then named infra slots. Use with `...` spread.
- */
- private static function build_authorize_call_args( \ReflectionMethod $method, object $principal, array $metadata, array $args, mixed $parent ): array {
- $call_args = array();
- foreach ( $method->getParameters() as $param ) {
- $name = $param->getName();
- if ( '_metadata' === $name ) {
- $call_args['_metadata'] = $metadata;
- } elseif ( '_args' === $name ) {
- $call_args['_args'] = $args;
- } elseif ( '_parent' === $name ) {
- $call_args['_parent'] = $parent;
- } elseif ( '' === $name || '_' !== $name[0] ) {
- // Principal — positional, must be the first entry in the spread.
- array_unshift( $call_args, $principal );
- }
- }
- return $call_args;
- }
-
- /**
- * Collect attribute instances declared on $source whose class declares an
- * authorization-shaped `authorize()` method.
- *
- * Mirrors {@see \Automattic\WooCommerce\Api\Infrastructure\DesignTime\ApiBuilder::collect_authorization_usages()}
- * for the runtime path: same direct-then-inherited precedence, same
- * "any class with a bool-returning authorize() method qualifies" rule.
- *
- * @param \ReflectionClass $source Class/trait/interface to read attributes from.
- *
- * @return array<int, object>
- */
- private static function collect_authorization_instances( \ReflectionClass $source ): array {
- $instances = array();
- foreach ( $source->getAttributes() as $attr ) {
- $name = $attr->getName();
- if ( ! class_exists( $name ) || ! method_exists( $name, 'authorize' ) ) {
- continue;
- }
- $method = new \ReflectionMethod( $name, 'authorize' );
- if ( ! self::authorize_method_shape_is_valid( $method ) ) {
- continue;
- }
- $instances[] = $attr->newInstance();
- }
- return $instances;
- }
-
- /**
- * Whether a method's shape matches the authorization-attribute contract:
- * public, non-static, returns bool, and parameters drawn from the accepted
- * set — at most one principal (any non-`_`-prefixed name, non-nullable
- * typed) plus any subset of `$_metadata` (array), `$_args` (array), and
- * `$_parent` (any type).
- *
- * Mirrors the build-time `ApiBuilder::validate_attribute_authorize_shape()`
- * check so the runtime helper recognises the same set of attributes ApiBuilder
- * would have emitted into a resolver.
- *
- * @param \ReflectionMethod $method The method to inspect.
- */
- private static function authorize_method_shape_is_valid( \ReflectionMethod $method ): bool {
- if ( $method->isStatic() || ! $method->isPublic() ) {
- return false;
- }
- $return_type = $method->getReturnType();
- if ( ! $return_type instanceof \ReflectionNamedType || 'bool' !== $return_type->getName() ) {
- return false;
- }
-
- $principal_seen = false;
- foreach ( $method->getParameters() as $param ) {
- $name = $param->getName();
- if ( '_metadata' === $name || '_args' === $name ) {
- $type = $param->getType();
- if ( ! $type instanceof \ReflectionNamedType || 'array' !== $type->getName() ) {
- return false;
- }
- continue;
- }
- if ( '_parent' === $name ) {
- continue;
- }
- if ( '' !== $name && '_' === $name[0] ) {
- // Unknown infra parameter — reject.
- return false;
- }
- if ( $principal_seen ) {
- return false;
- }
- $type = $param->getType();
- if ( ! $type instanceof \ReflectionNamedType || $type->allowsNull() ) {
- return false;
- }
- $principal_seen = true;
- }
- return true;
- }
-
- /**
- * Invoke a callable, translating any thrown exception into a
- * spec-compliant GraphQL error with a machine-readable code.
- *
- * - ApiException → its own code + extensions, with the original message.
- * - InvalidArgumentException → INVALID_ARGUMENT, with the original message.
- * - Any other Throwable → INTERNAL_ERROR, with a generic message; the
- * original throwable is attached as `previous` for debug-mode surfacing.
- *
- * Public so that generated resolvers can wrap Code-API calls that happen
- * outside the execute()/authorize() pair (e.g. the Connection::slice()
- * call emitted for nested paginated connection fields, which can throw
- * InvalidArgumentException when pagination bounds are exceeded).
- *
- * @param callable $operation Callable to invoke.
- *
- * @return mixed The return value of the callable.
- * @throws Error On any exception from the callable.
- */
- public static function translate_exceptions( callable $operation ): mixed {
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- try {
- return $operation();
- } catch ( \Automattic\WooCommerce\Api\ApiException $e ) {
- // Caller-supplied extensions come first so the canonical
- // getErrorCode() can't be silently overridden by an extensions
- // entry keyed 'code'. The invariant "the code on the wire
- // equals ApiException::getErrorCode()" is worth enforcing.
- throw new Error(
- $e->getMessage(),
- extensions: array_merge(
- $e->getExtensions(),
- array( 'code' => $e->getErrorCode() )
- )
- );
- } catch ( \InvalidArgumentException $e ) {
- throw new Error(
- $e->getMessage(),
- extensions: array( 'code' => 'INVALID_ARGUMENT' )
- );
- } catch ( \Throwable $e ) {
- throw new Error(
- 'An unexpected error occurred.',
- previous: $e,
- extensions: array( 'code' => 'INTERNAL_ERROR' )
- );
- }//end try
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
-
- /**
- * Lazy-initialize and return the WP_Filesystem global, or null when the
- * direct method isn't available (e.g. credentials prompt would be needed).
- */
- public static function wp_filesystem(): ?\WP_Filesystem_Base {
- global $wp_filesystem;
- if ( ! $wp_filesystem ) {
- require_once ABSPATH . 'wp-admin/includes/file.php';
- if ( ! WP_Filesystem() ) {
- return null;
- }
- }
- return $wp_filesystem;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/CustomScalarType.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/CustomScalarType.php
deleted file mode 100644
index 9e7ce948461..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/CustomScalarType.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's CustomScalarType, used
- * by autogenerated custom scalar types.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `name`, `description`, `serialize`,
- * `parseValue`, `parseLiteral`).
- *
- * A `metadata` config key may also be provided; see
- * {@see ObjectType::get_metadata()} for the semantics.
- */
-class CustomScalarType extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType {
- /**
- * Type-level metadata entries declared in the config, keyed by name.
- *
- * @return array<string, bool|int|float|string|null>
- */
- public function get_metadata(): array {
- return $this->config['metadata'] ?? array();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/EnumType.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/EnumType.php
deleted file mode 100644
index 8d7acdff321..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/EnumType.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's EnumType, used by
- * autogenerated enums.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `name`, `description`, `values`).
- *
- * A `metadata` config key may also be provided; see
- * {@see ObjectType::get_metadata()} for the semantics.
- */
-class EnumType extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType {
- /**
- * Type-level metadata entries declared in the config, keyed by name.
- *
- * @return array<string, bool|int|float|string|null>
- */
- public function get_metadata(): array {
- return $this->config['metadata'] ?? array();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/Error.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/Error.php
deleted file mode 100644
index 3616f33f0ae..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/Error.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's Error, used by
- * autogenerated resolvers when they need to surface a GraphQL-spec error
- * directly (e.g. the UNAUTHORIZED error an autogenerated authorize()-backed
- * resolver throws before invoking the command).
- *
- * Behaviour is inherited verbatim, including the named `extensions`
- * argument that callers rely on to attach an error code.
- */
-class Error extends \Automattic\WooCommerce\Vendor\GraphQL\Error\Error {
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/InputObjectType.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/InputObjectType.php
deleted file mode 100644
index e2d29e817de..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/InputObjectType.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's InputObjectType, used
- * by autogenerated input types.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `name`, `description`, `fields`).
- *
- * A `metadata` config key may also be provided; see
- * {@see ObjectType::get_metadata()} for the semantics.
- */
-class InputObjectType extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType {
- /**
- * Type-level metadata entries declared in the config, keyed by name.
- *
- * @return array<string, bool|int|float|string|null>
- */
- public function get_metadata(): array {
- return $this->config['metadata'] ?? array();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/InterfaceType.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/InterfaceType.php
deleted file mode 100644
index f192c260206..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/InterfaceType.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's InterfaceType, used by
- * autogenerated interface types.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `name`, `description`, `fields`,
- * `resolveType`).
- *
- * A `metadata` config key may also be provided; see
- * {@see ObjectType::get_metadata()} for the semantics.
- */
-class InterfaceType extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType {
- /**
- * Type-level metadata entries declared in the config, keyed by name.
- *
- * @return array<string, bool|int|float|string|null>
- */
- public function get_metadata(): array {
- return $this->config['metadata'] ?? array();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/ObjectType.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/ObjectType.php
deleted file mode 100644
index 32321ab783a..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/ObjectType.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's ObjectType, used by
- * autogenerated output types, pagination types and root Query/Mutation types.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `name`, `description`, `fields`,
- * `interfaces`). The `fields` entry is either an array or a callable
- * returning an array of field definitions.
- *
- * The wrapper also recognises a `metadata` key — an associative array
- * mapping metadata `name` => scalar `value` — that ApiBuilder emits for
- * types carrying {@see \Automattic\WooCommerce\Api\Attributes\Metadata}
- * attributes. The engine ignores unknown config keys, so this rides through
- * untouched and is surfaced by {@see self::get_metadata()}.
- */
-class ObjectType extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType {
- /**
- * Type-level metadata entries declared in the config, keyed by name.
- *
- * @return array<string, bool|int|float|string|null>
- */
- public function get_metadata(): array {
- return $this->config['metadata'] ?? array();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/README.md b/plugins/woocommerce/src/Api/Infrastructure/Schema/README.md
deleted file mode 100644
index 8fbed1cceab..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/README.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# GraphQL Schema Surface
-
-This directory is the sole point of contact between autogenerated GraphQL code and the underlying GraphQL engine (currently webonyx/graphql-php, renamespaced under `Automattic\WooCommerce\Vendor\GraphQL\*`).
-
-## Why it exists
-
-The dual-code API architecture treats the GraphQL engine as an implementation detail: given a set of code-API classes under `src/Api/`, ApiBuilder regenerates the autogenerated tree, and switching engines should be a matter of updating templates and regenerating. That contract only holds as long as the autogenerated output never references engine-specific symbols directly.
-
-WooCommerce plugins that reuse this infrastructure commit their autogenerated trees to their own repos. If those trees imported from `Automattic\WooCommerce\Vendor\GraphQL\*`, an engine switch in WooCommerce would break every already-committed plugin. Routing every engine reference through this namespace prevents that: the generator emits imports from `Automattic\WooCommerce\Api\Infrastructure\Schema\*` only, and the classes here translate to whichever engine is current.
-
-## What's in here
-
-Every symbol a generated resolver, type, or root-type class can touch at runtime:
-
-| Symbol | Used by generated code as |
-| --- | --- |
-| `Schema` | Root schema object constructed in each autogenerated `GraphQLController::build_schema()`. |
-| `ObjectType` | Output types, pagination connection/edge types, root Query/Mutation, scalar-result wrappers. |
-| `InputObjectType` | Input types. |
-| `EnumType` | Enums. |
-| `InterfaceType` | Interface types. |
-| `CustomScalarType` | Custom scalars. |
-| `Type` | Static facade: `int()`, `string()`, `boolean()`, `float()`, `id()`, `nonNull($inner)`, `listOf($inner)`. |
-| `Error` | Thrown directly from resolver code when surfacing a GraphQL-spec error (e.g. the `UNAUTHORIZED` error in authorize()-backed resolvers). |
-| `ResolveInfo` | Fourth-parameter type hint on every resolver's `resolve()` method. Registered via `class_alias` in `aliases.php`. |
-| `AST\StringValueNode` | Referenced via `instanceof` inside custom scalar `parseLiteral()` callbacks. Registered via `class_alias` in `aliases.php`. |
-
-## Three implementation patterns
-
-- **Subclass** (`Schema`, `ObjectType`, `InputObjectType`, `EnumType`, `InterfaceType`, `CustomScalarType`, `Error`) — extends the webonyx class with an empty body. Today the subclass is a no-op indirection; in a future migration the constructor would translate the webonyx-shaped config array into whatever the new engine expects. Webonyx accepts subclasses of its own types wherever it accepts the parent, so there's no runtime friction today.
-- **Static facade** (`Type`) — delegates each static method to the webonyx equivalent. Return types are intentionally omitted so a future migration can change the concrete return class without breaking callers.
-- **Class alias** (`ResolveInfo`, `AST\StringValueNode`) — used when the engine itself constructs the instances and hands them to resolver code. Subclassing doesn't help because the engine creates the parent class directly. The aliases are registered eagerly in `aliases.php`, wired in via `composer.json`'s `autoload.files` entry so they run at every boot (plain Composer autoload and the Jetpack autoloader both honour this list). A future engine switch replaces the alias with a real class whose public shape matches what generated code expects.
-
-## Rules
-
-- **No implementation logic in the subclasses.** They exist to be stable FQCNs, nothing more. Behaviour that would diverge from the webonyx parent is engine-specific and belongs in a per-engine adapter, not in the surface.
-- **Generated code references this namespace only, never `Vendor\GraphQL\*`.** If a template needs a webonyx symbol that isn't here, add it here first.
-- **Versioning is implicit in the namespace.** If a future change would break already-committed plugin code, add a sibling namespace (e.g. `Api\Infrastructure\Schema\V2`) and teach ApiBuilder to emit against it; keep the current surface until the last dependent plugin has migrated.
-
-## Adding a new symbol
-
-1. Add a subclass / facade method / alias file in the matching style.
-2. Update the template that needs it to import from this namespace.
-3. Regenerate core (`pnpm build:api`) and confirm the Autogenerated/ diff is imports-only.
-4. Add a row to the table above.
-
-## Engine migration checklist
-
-If WooCommerce switches off webonyx, the changes localized to this directory are:
-
-1. Each subclass's constructor accepts the webonyx-shaped config array and translates internally to the new engine's shape.
-2. Each class alias becomes a real class whose public members mirror what generated code accesses.
-3. `Type`'s static methods return the new engine's equivalents.
-
-Generated code already committed to plugin repos keeps working without the plugins having to regenerate.
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/Schema.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/Schema.php
deleted file mode 100644
index ff6f5e2edeb..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/Schema.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-/**
- * Stable subclass of the underlying GraphQL engine's Schema, used by
- * autogenerated GraphQLController subclasses.
- *
- * The constructor accepts the same associative-array config the current
- * engine (webonyx) documents (keys: `query`, `mutation`, `types`, etc.).
- * Extending the engine class means its executor treats an instance of this
- * class identically to its own parent, so no adaptation is needed at runtime.
- */
-class Schema extends \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema {
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/Type.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/Type.php
deleted file mode 100644
index 8c199891a16..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/Type.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Infrastructure\Schema;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type as WebonyxType;
-
-/**
- * Static facade for GraphQL scalar types and type modifiers referenced by
- * autogenerated resolvers.
- *
- * Autogenerated code emitted by ApiBuilder only touches the underlying
- * GraphQL engine through this (and the other classes in the
- * Api\Infrastructure\Schema namespace), so the engine can be swapped without
- * invalidating already-committed generated code. Return types are
- * intentionally omitted so a future migration can change the concrete
- * return type without breaking callers.
- */
-final class Type {
- /**
- * The built-in GraphQL Int scalar.
- */
- public static function int() {
- return WebonyxType::int();
- }
-
- /**
- * The built-in GraphQL String scalar.
- */
- public static function string() {
- return WebonyxType::string();
- }
-
- /**
- * The built-in GraphQL Boolean scalar.
- */
- public static function boolean() {
- return WebonyxType::boolean();
- }
-
- /**
- * The built-in GraphQL Float scalar.
- */
- public static function float() {
- return WebonyxType::float();
- }
-
- /**
- * The built-in GraphQL ID scalar.
- */
- public static function id() {
- return WebonyxType::id();
- }
-
- // phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Method names mirror the webonyx Type factories so autogenerated imports of this facade and its webonyx counterpart are interchangeable.
-
- /**
- * Wrap a nullable schema type as non-null (`T!`).
- *
- * @param mixed $inner A nullable schema type.
- */
- public static function nonNull( $inner ) {
- return WebonyxType::nonNull( $inner );
- }
-
- /**
- * Wrap a schema type as a list (`[T]`).
- *
- * @param mixed $inner A schema type.
- */
- public static function listOf( $inner ) {
- return WebonyxType::listOf( $inner );
- }
-
- // phpcs:enable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
-}
diff --git a/plugins/woocommerce/src/Api/Infrastructure/Schema/aliases.php b/plugins/woocommerce/src/Api/Infrastructure/Schema/aliases.php
deleted file mode 100644
index ce2a43b89e1..00000000000
--- a/plugins/woocommerce/src/Api/Infrastructure/Schema/aliases.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-/**
- * Class-alias bootstrap for the Api\Infrastructure\Schema surface.
- *
- * Some symbols in the surface — ResolveInfo and StringValueNode — cannot be
- * subclasses because the GraphQL engine constructs them itself and hands them
- * to resolver code. A subclass would be a distinct type and fail resolver
- * parameter type-hint checks. Instead we register them as class_alias of
- * their engine counterparts so the two FQCNs resolve to the same class.
- *
- * This file is loaded eagerly via composer's `autoload.files` entry (which
- * the Jetpack autoloader in turn exposes through its filemap), so the aliases
- * are available before any resolver is invoked.
- */
-
-declare(strict_types=1);
-
-class_alias(
- \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo::class,
- 'Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\ResolveInfo'
-);
-
-class_alias(
- \Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode::class,
- 'Automattic\\WooCommerce\\Api\\Infrastructure\\Schema\\AST\\StringValueNode'
-);
diff --git a/plugins/woocommerce/src/Api/InputTypes/Coupons/CreateCouponInput.php b/plugins/woocommerce/src/Api/InputTypes/Coupons/CreateCouponInput.php
deleted file mode 100644
index 1ec7ea7c38f..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Coupons/CreateCouponInput.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-
-/**
- * Input type for creating a coupon.
- */
-#[Description( 'Data required to create a new coupon.' )]
-class CreateCouponInput {
- use TracksProvidedFields;
-
- #[Description( 'The coupon code.' )]
- public string $code;
-
- #[Description( 'The coupon description.' )]
- public ?string $description = null;
-
- #[Description( 'The type of discount.' )]
- public ?DiscountType $discount_type = null;
-
- #[Description( 'The discount amount.' )]
- public ?float $amount = null;
-
- #[Description( 'The coupon status.' )]
- public ?CouponStatus $status = null;
-
- #[Description( 'The date the coupon expires (ISO 8601).' )]
- public ?string $date_expires = null;
-
- #[Description( 'Whether the coupon can only be used alone.' )]
- public ?bool $individual_use = null;
-
- #[Description( 'Product IDs the coupon can be applied to.' )]
- #[ArrayOf( 'int' )]
- public ?array $product_ids = null;
-
- #[Description( 'Product IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public ?array $excluded_product_ids = null;
-
- #[Description( 'Maximum number of times the coupon can be used in total.' )]
- public ?int $usage_limit = null;
-
- #[Description( 'Maximum number of times the coupon can be used per customer.' )]
- public ?int $usage_limit_per_user = null;
-
- #[Description( 'Maximum number of items the coupon can be applied to.' )]
- public ?int $limit_usage_to_x_items = null;
-
- #[Description( 'Whether the coupon grants free shipping.' )]
- public ?bool $free_shipping = null;
-
- #[Description( 'Product category IDs the coupon applies to.' )]
- #[ArrayOf( 'int' )]
- public ?array $product_categories = null;
-
- #[Description( 'Product category IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public ?array $excluded_product_categories = null;
-
- #[Description( 'Whether the coupon excludes items on sale.' )]
- public ?bool $exclude_sale_items = null;
-
- #[Description( 'Minimum order amount required to use the coupon.' )]
- public ?float $minimum_amount = null;
-
- #[Description( 'Maximum order amount allowed to use the coupon.' )]
- public ?float $maximum_amount = null;
-
- #[Description( 'Email addresses that can use this coupon.' )]
- #[ArrayOf( 'string' )]
- public ?array $email_restrictions = null;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Coupons/UpdateCouponInput.php b/plugins/woocommerce/src/Api/InputTypes/Coupons/UpdateCouponInput.php
deleted file mode 100644
index 587ec9e0764..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Coupons/UpdateCouponInput.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-
-/**
- * Input type for updating a coupon.
- */
-#[Description( 'Data for updating an existing coupon. All fields are optional.' )]
-class UpdateCouponInput {
- use TracksProvidedFields;
-
- #[Description( 'The ID of the coupon to update.' )]
- public int $id;
-
- #[Description( 'The coupon code.' )]
- public ?string $code = null;
-
- #[Description( 'The coupon description.' )]
- public ?string $description = null;
-
- #[Description( 'The type of discount.' )]
- public ?DiscountType $discount_type = null;
-
- #[Description( 'The discount amount.' )]
- public ?float $amount = null;
-
- #[Description( 'The coupon status.' )]
- public ?CouponStatus $status = null;
-
- #[Description( 'The date the coupon expires (ISO 8601).' )]
- public ?string $date_expires = null;
-
- #[Description( 'Whether the coupon can only be used alone.' )]
- public ?bool $individual_use = null;
-
- #[Description( 'Product IDs the coupon can be applied to.' )]
- #[ArrayOf( 'int' )]
- public ?array $product_ids = null;
-
- #[Description( 'Product IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public ?array $excluded_product_ids = null;
-
- #[Description( 'Maximum number of times the coupon can be used in total.' )]
- public ?int $usage_limit = null;
-
- #[Description( 'Maximum number of times the coupon can be used per customer.' )]
- public ?int $usage_limit_per_user = null;
-
- #[Description( 'Maximum number of items the coupon can be applied to.' )]
- public ?int $limit_usage_to_x_items = null;
-
- #[Description( 'Whether the coupon grants free shipping.' )]
- public ?bool $free_shipping = null;
-
- #[Description( 'Product category IDs the coupon applies to.' )]
- #[ArrayOf( 'int' )]
- public ?array $product_categories = null;
-
- #[Description( 'Product category IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public ?array $excluded_product_categories = null;
-
- #[Description( 'Whether the coupon excludes items on sale.' )]
- public ?bool $exclude_sale_items = null;
-
- #[Description( 'Minimum order amount required to use the coupon.' )]
- public ?float $minimum_amount = null;
-
- #[Description( 'Maximum order amount allowed to use the coupon.' )]
- public ?float $maximum_amount = null;
-
- #[Description( 'Email addresses that can use this coupon.' )]
- #[ArrayOf( 'string' )]
- public ?array $email_restrictions = null;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Products/BaseProductInput.php b/plugins/woocommerce/src/Api/InputTypes/Products/BaseProductInput.php
deleted file mode 100644
index 5dca72068ac..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Products/BaseProductInput.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\Enums\Products\ProductType;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-
-/**
- * Shared fields for product creation and update input types.
- */
-abstract class BaseProductInput {
- use TracksProvidedFields;
-
- #[Description( 'The product slug.' )]
- public ?string $slug = null;
-
- #[Description( 'The product SKU.' )]
- public ?string $sku = null;
-
- #[Description( 'The full product description.' )]
- public ?string $description = null;
-
- #[Description( 'The short product description.' )]
- public ?string $short_description = null;
-
- #[Description( 'The product status.' )]
- public ?ProductStatus $status = null;
-
- #[Description( 'The product type.' )]
- public ?ProductType $product_type = null;
-
- #[Description( 'The regular price.' )]
- public ?float $regular_price = null;
-
- #[Description( 'The sale price.' )]
- public ?float $sale_price = null;
-
- #[Description( 'Whether to manage stock.' )]
- public ?bool $manage_stock = null;
-
- #[Description( 'The number of items in stock.' )]
- public ?int $stock_quantity = null;
-
- #[Description( 'The product dimensions.' )]
- public ?DimensionsInput $dimensions = null;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Products/CreateProductInput.php b/plugins/woocommerce/src/Api/InputTypes/Products/CreateProductInput.php
deleted file mode 100644
index b115190532b..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Products/CreateProductInput.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Input type for creating a product.
- */
-#[Description( 'Data required to create a new product.' )]
-class CreateProductInput extends BaseProductInput {
- #[Description( 'The product name.' )]
- public string $name;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Products/DimensionsInput.php b/plugins/woocommerce/src/Api/InputTypes/Products/DimensionsInput.php
deleted file mode 100644
index f1b50053374..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Products/DimensionsInput.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-
-/**
- * Input type for product dimensions.
- */
-#[Description( 'Physical dimensions and weight for a product.' )]
-class DimensionsInput {
- use TracksProvidedFields;
-
- #[Description( 'The product length.' )]
- public ?float $length = null;
-
- #[Description( 'The product width.' )]
- public ?float $width = null;
-
- #[Description( 'The product height.' )]
- public ?float $height = null;
-
- #[Description( 'The product weight.' )]
- public ?float $weight = null;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Products/ProductFilterInput.php b/plugins/woocommerce/src/Api/InputTypes/Products/ProductFilterInput.php
deleted file mode 100644
index 0b9f0802e4c..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Products/ProductFilterInput.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-
-/**
- * Input type for filtering products.
- *
- * Used with parameter-level #[Unroll] to expand fields as direct query arguments.
- * Uses constructor promotion so the builder can instantiate it via named arguments.
- */
-#[Description( 'Filter criteria for listing products.' )]
-class ProductFilterInput {
- use TracksProvidedFields;
-
- /**
- * Constructor.
- *
- * @param ?ProductStatus $status Filter by product status.
- * @param ?StockStatus $stock_status Filter by stock status.
- * @param ?string $search Search products by keyword.
- */
- public function __construct(
- #[Description( 'Filter by product status.' )]
- public readonly ?ProductStatus $status = null,
- #[Description( 'Filter by stock status.' )]
- public readonly ?StockStatus $stock_status = null,
- #[Description( 'Search products by keyword.' )]
- public readonly ?string $search = null,
- ) {
- }
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/Products/UpdateProductInput.php b/plugins/woocommerce/src/Api/InputTypes/Products/UpdateProductInput.php
deleted file mode 100644
index 4f0e13f5b3c..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/Products/UpdateProductInput.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Input type for updating a product.
- */
-#[Description( 'Data for updating an existing product.' )]
-class UpdateProductInput extends BaseProductInput {
- #[Description( 'The ID of the product to update.' )]
- public int $id;
-
- #[Description( 'The product name.' )]
- public ?string $name = null;
-}
diff --git a/plugins/woocommerce/src/Api/InputTypes/TracksProvidedFields.php b/plugins/woocommerce/src/Api/InputTypes/TracksProvidedFields.php
deleted file mode 100644
index fdb8beb78fc..00000000000
--- a/plugins/woocommerce/src/Api/InputTypes/TracksProvidedFields.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\InputTypes;
-
-/**
- * Trait for input types to track which fields were explicitly provided in the GraphQL request.
- *
- * This allows mutations to distinguish between a field being missing (don't change it)
- * and explicitly set to null (clear it).
- */
-trait TracksProvidedFields {
- /**
- * Fields that were explicitly provided in the input.
- *
- * Using an underscore prefix to keep it invisible to the ApiBuilder
- * (which only scans public properties for GraphQL fields).
- *
- * @var array<string, true>
- */
- protected array $provided_fields = array(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase -- internal tracking array
-
- /**
- * Mark a field as explicitly provided in the input.
- *
- * @param string $field The field name.
- */
- public function mark_provided( string $field ): void {
- $this->provided_fields[ $field ] = true;
- }
-
- /**
- * Check whether a field was explicitly provided in the input.
- *
- * @param string $field The field name.
- * @return bool
- */
- public function was_provided( string $field ): bool {
- return isset( $this->provided_fields[ $field ] );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Interfaces/ObjectWithId.php b/plugins/woocommerce/src/Api/Interfaces/ObjectWithId.php
deleted file mode 100644
index 18254488856..00000000000
--- a/plugins/woocommerce/src/Api/Interfaces/ObjectWithId.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Interfaces;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Interface trait for objects that have a numeric ID.
- */
-#[Description( 'An object with a numeric ID.' )]
-trait ObjectWithId {
- /**
- * The unique numeric identifier.
- *
- * @var int
- */
- #[Description( 'The unique numeric identifier.' )]
- public int $id;
-}
diff --git a/plugins/woocommerce/src/Api/Interfaces/Product.php b/plugins/woocommerce/src/Api/Interfaces/Product.php
deleted file mode 100644
index a4a3f1443c8..00000000000
--- a/plugins/woocommerce/src/Api/Interfaces/Product.php
+++ /dev/null
@@ -1,212 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Interfaces;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Deprecated;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\Parameter;
-use Automattic\WooCommerce\Api\Attributes\ParameterDescription;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\Enums\Products\ProductType;
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Scalars\DateTime;
-use Automattic\WooCommerce\Api\Types\Products\ProductDimensions;
-use Automattic\WooCommerce\Api\Types\Products\ProductAttribute;
-use Automattic\WooCommerce\Api\Types\Products\ProductImage;
-use Automattic\WooCommerce\Api\Types\Products\ProductReview;
-
-/**
- * Interface trait for WooCommerce products.
- *
- * Defines the common fields shared by all product types.
- */
-#[Name( 'Product' )]
-#[Description( 'A WooCommerce product.' )]
-trait Product {
- use ObjectWithId;
-
- /**
- * The product name.
- *
- * @var string
- */
- #[Description( 'The product name.' )]
- public string $name;
-
- /**
- * The product slug.
- *
- * @var string
- */
- #[Description( 'The product slug.' )]
- public string $slug;
-
- /**
- * The product SKU.
- *
- * @var ?string
- */
- #[Description( 'The product SKU.' )]
- public ?string $sku;
-
- /**
- * The full product description.
- *
- * @var string
- */
- #[Description( 'The full product description.' )]
- public string $description;
-
- /**
- * The short product description.
- *
- * @var string
- */
- #[Deprecated( 'Use description instead.' )]
- #[Description( 'The short product description.' )]
- public string $short_description;
-
- /**
- * The product status.
- *
- * @var ProductStatus
- */
- #[Description( 'The product status.' )]
- public ProductStatus $status;
-
- /**
- * The raw status as stored in WordPress. Useful when status is OTHER.
- *
- * @var string
- */
- #[Description( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).' )]
- public string $raw_status;
-
- /**
- * The product type.
- *
- * @var ProductType
- */
- #[Description( 'The product type.' )]
- public ProductType $product_type;
-
- /**
- * The raw product type as stored in WooCommerce. Useful when product_type is OTHER.
- *
- * @var string
- */
- #[Description( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).' )]
- public string $raw_product_type;
-
- /**
- * The regular price of the product. Null when not set.
- *
- * @var ?string
- */
- #[Description( 'The regular price of the product. Null when not set.' )]
- #[Parameter( name: 'formatted', type: 'bool', default: true, description: 'Whether to apply currency formatting.' )]
- public ?string $regular_price;
-
- /**
- * The sale price of the product.
- *
- * @var ?string
- */
- #[Description( 'The sale price of the product.' )]
- #[Parameter( name: 'formatted', type: 'bool', default: true )]
- #[ParameterDescription( name: 'formatted', description: 'When true, returns price with currency symbol.' )]
- public ?string $sale_price;
-
- /**
- * The stock status of the product.
- *
- * @var StockStatus
- */
- #[Description( 'The stock status of the product.' )]
- public StockStatus $stock_status;
-
- /**
- * The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER.
- *
- * @var string
- */
- #[Description( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).' )]
- public string $raw_stock_status;
-
- /**
- * The number of items in stock.
- *
- * @var ?int
- */
- #[Description( 'The number of items in stock.' )]
- public ?int $stock_quantity;
-
- /**
- * The product dimensions.
- *
- * @var ?ProductDimensions
- */
- #[Description( 'The product dimensions.' )]
- public ?ProductDimensions $dimensions;
-
- /**
- * The product images.
- *
- * @var ProductImage[]
- */
- #[Description( 'The product images.' )]
- #[ArrayOf( ProductImage::class )]
- public array $images;
-
- /**
- * The product attributes.
- *
- * @var ProductAttribute[]
- */
- #[Description( 'The product attributes.' )]
- #[ArrayOf( ProductAttribute::class )]
- public array $attributes;
-
- /**
- * Customer reviews for this product.
- *
- * @var Connection
- */
- #[Description( 'Customer reviews for this product.' )]
- #[ConnectionOf( ProductReview::class )]
- public Connection $reviews;
-
- /**
- * The date the product was created.
- *
- * @var ?string
- */
- #[Description( 'The date the product was created.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_created;
-
- /**
- * The date the product was last modified.
- *
- * @var ?string
- */
- #[Description( 'The date the product was last modified.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_modified;
-
- /**
- * Internal notes (ignored in schema).
- *
- * @var ?string
- */
- #[Ignore]
- public ?string $internal_notes;
-}
diff --git a/plugins/woocommerce/src/Api/InvalidTokenException.php b/plugins/woocommerce/src/Api/InvalidTokenException.php
deleted file mode 100644
index b9dd509f86e..00000000000
--- a/plugins/woocommerce/src/Api/InvalidTokenException.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Thrown to signal that authentication credentials were supplied but are
- * invalid, e.g. an unrecognised API token, a malformed Authorization header,
- * or expired credentials.
- *
- * Use this when the caller *did* attempt to authenticate but the credentials
- * themselves were rejected. For "no credentials at all" use
- * {@see UnauthorizedException}.
- *
- * Wire shape: `extensions.code = 'INVALID_TOKEN'`, HTTP status 401.
- */
-class InvalidTokenException extends ApiException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param array $extensions Additional error metadata to surface in the GraphQL `extensions` object.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message = 'Invalid credentials.',
- array $extensions = array(),
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, 'INVALID_TOKEN', $extensions, 401, $previous );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Coupons/CreateCoupon.php b/plugins/woocommerce/src/Api/Mutations/Coupons/CreateCoupon.php
deleted file mode 100644
index d5f60c351db..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Coupons/CreateCoupon.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\InputTypes\Coupons\CreateCouponInput;
-use Automattic\WooCommerce\Api\Utils\Coupons\CouponMapper;
-use Automattic\WooCommerce\Api\Types\Coupons\Coupon;
-
-/**
- * Mutation to create a new coupon.
- */
-#[Description( 'Create a new coupon.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-class CreateCoupon {
- /**
- * Execute the mutation.
- *
- * @param CreateCouponInput $input The coupon creation data.
- * @return Coupon
- */
- public function execute(
- #[Description( 'Data for the new coupon.' )]
- CreateCouponInput $input,
- ): Coupon {
- $wc_coupon = new \WC_Coupon();
- $wc_coupon->set_code( $input->code );
-
- foreach ( array( 'description', 'amount', 'date_expires', 'individual_use', 'product_ids', 'excluded_product_ids', 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'free_shipping', 'product_categories', 'excluded_product_categories', 'exclude_sale_items', 'minimum_amount', 'maximum_amount', 'email_restrictions' ) as $field ) {
- if ( null !== $input->$field ) {
- $wc_coupon->{"set_{$field}"}( $input->$field );
- }
- }
-
- if ( null !== $input->discount_type ) {
- $wc_coupon->set_discount_type( $input->discount_type->value );
- }
- if ( null !== $input->status ) {
- $wc_coupon->set_status( $input->status->value );
- }
-
- $wc_coupon->save();
-
- return CouponMapper::from_wc_coupon( $wc_coupon );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Coupons/DeleteCoupon.php b/plugins/woocommerce/src/Api/Mutations/Coupons/DeleteCoupon.php
deleted file mode 100644
index fbe77a0189d..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Coupons/DeleteCoupon.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Types\Coupons\DeleteCouponResult;
-
-/**
- * Mutation to delete a coupon.
- */
-#[Description( 'Delete a coupon.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-class DeleteCoupon {
- /**
- * Execute the mutation.
- *
- * @param int $id The coupon ID.
- * @param bool $force Whether to permanently delete.
- * @return DeleteCouponResult
- * @throws ApiException When the coupon is not found.
- */
- public function execute(
- #[Description( 'The ID of the coupon to delete.' )]
- int $id,
- #[Description( 'Whether to permanently delete the coupon (bypass trash).' )]
- bool $force = false,
- ): DeleteCouponResult {
- $wc_coupon = new \WC_Coupon( $id );
-
- if ( ! $wc_coupon->get_id() ) {
- throw new ApiException( 'Coupon not found.', 'NOT_FOUND', status_code: 404 );
- }
-
- // Capture the raw return value. A `(bool)` cast would coerce
- // filter-originated `WP_Error` objects to `true`, reporting failure
- // as success; we need to detect that case explicitly and surface
- // the underlying error instead.
- $deleted = $wc_coupon->delete( $force );
-
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- if ( $deleted instanceof \WP_Error ) {
- throw new ApiException(
- $deleted->get_error_message(),
- 'INTERNAL_ERROR',
- status_code: 500,
- );
- }
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
-
- $result = new DeleteCouponResult();
- $result->id = $id;
- $result->deleted = true === $deleted;
-
- return $result;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Coupons/UpdateCoupon.php b/plugins/woocommerce/src/Api/Mutations/Coupons/UpdateCoupon.php
deleted file mode 100644
index 6ee4f522f28..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Coupons/UpdateCoupon.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\InputTypes\Coupons\UpdateCouponInput;
-use Automattic\WooCommerce\Api\Utils\Coupons\CouponMapper;
-use Automattic\WooCommerce\Api\Types\Coupons\Coupon;
-
-/**
- * Mutation to update an existing coupon.
- */
-#[Description( 'Update an existing coupon.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-class UpdateCoupon {
- /**
- * Execute the mutation.
- *
- * @param UpdateCouponInput $input The fields to update.
- * @return Coupon
- * @throws ApiException When the coupon is not found.
- */
- public function execute(
- #[Description( 'The fields to update.' )]
- UpdateCouponInput $input,
- ): Coupon {
- $wc_coupon = new \WC_Coupon( $input->id );
-
- if ( ! $wc_coupon->get_id() ) {
- throw new ApiException( 'Coupon not found.', 'NOT_FOUND', status_code: 404 );
- }
-
- foreach ( array( 'code', 'description', 'amount', 'date_expires', 'individual_use', 'product_ids', 'excluded_product_ids', 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items', 'free_shipping', 'product_categories', 'excluded_product_categories', 'exclude_sale_items', 'minimum_amount', 'maximum_amount', 'email_restrictions' ) as $field ) {
- if ( $input->was_provided( $field ) ) {
- $wc_coupon->{"set_{$field}"}( $input->$field );
- }
- }
-
- // Nullable enums: only invoke the setter when the client supplied a
- // non-null value. An explicit null means "ignore this field" here —
- // WC_Coupon's enum setters don't accept null and would fall back to
- // their defaults (e.g. 'fixed_cart' for discount_type), silently
- // overwriting whatever is already on the coupon.
- if ( $input->was_provided( 'discount_type' ) && null !== $input->discount_type ) {
- $wc_coupon->set_discount_type( $input->discount_type->value );
- }
- if ( $input->was_provided( 'status' ) && null !== $input->status ) {
- $wc_coupon->set_status( $input->status->value );
- }
-
- $wc_coupon->save();
-
- return CouponMapper::from_wc_coupon( $wc_coupon );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Products/CreateProduct.php b/plugins/woocommerce/src/Api/Mutations/Products/CreateProduct.php
deleted file mode 100644
index 13cb0574277..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Products/CreateProduct.php
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ReturnType;
-use Automattic\WooCommerce\Api\InputTypes\Products\CreateProductInput;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-use Automattic\WooCommerce\Api\Traits\RequiresManageWoocommerce;
-use Automattic\WooCommerce\Api\Utils\Products\ProductMapper;
-use Automattic\WooCommerce\Api\Utils\Products\ProductRepository;
-
-/**
- * Mutation to create a new product.
- *
- * Demonstrates: DI via init(), inherited capability (trait), ApiException with extensions.
- */
-#[Description( 'Create a new product.' )]
-#[RequiredCapability( 'edit_products' )]
-class CreateProduct {
- use RequiresManageWoocommerce;
-
- /**
- * The product repository.
- *
- * @var ProductRepository
- */
- private ProductRepository $repository;
-
- /**
- * Inject dependencies via the DI container.
- *
- * @internal
- *
- * @param ProductRepository $repository The product repository.
- */
- final public function init( ProductRepository $repository ): void {
- $this->repository = $repository;
- }
-
- /**
- * Execute the mutation.
- *
- * @param CreateProductInput $input The product creation data.
- * @return object
- * @throws ApiException When validation fails.
- */
- #[ReturnType( Product::class )]
- public function execute(
- #[Description( 'Data for the new product.' )]
- CreateProductInput $input,
- ): object {
- // Best-effort duplicate-name check. There is an inherent TOCTOU race
- // here: two nearly-simultaneous requests with the same name can both
- // pass this check and both succeed in creating the product, because
- // wp_posts.post_title is not a unique column in the schema and WP
- // offers no portable atomic "reserve name" primitive. Locking via
- // wp_cache_add() would help only on sites with a persistent object
- // cache (Redis/Memcached), so we do not rely on it here. If strict
- // uniqueness is ever required, callers should enforce it at a
- // higher layer (e.g. a mutex around the REST handler) rather than
- // assume the API guarantees it.
- $existing = new \WP_Query(
- array(
- 'post_type' => 'product',
- 'title' => $input->name,
- 'post_status' => array( 'publish', 'draft', 'pending', 'private' ),
- 'fields' => 'ids',
- )
- );
-
- if ( $existing->found_posts > 0 ) {
- throw new ApiException(
- 'A product with this name already exists.',
- 'VALIDATION_ERROR',
- array( 'field' => 'name' ),
- 422,
- );
- }
-
- $wc_product = new \WC_Product();
- $wc_product->set_name( $input->name );
-
- foreach ( array( 'slug', 'sku', 'description', 'short_description', 'manage_stock', 'stock_quantity' ) as $field ) {
- if ( null !== $input->$field ) {
- $wc_product->{"set_{$field}"}( $input->$field );
- }
- }
-
- foreach ( array( 'regular_price', 'sale_price' ) as $field ) {
- if ( null !== $input->$field ) {
- $wc_product->{"set_{$field}"}( (string) $input->$field );
- }
- }
-
- if ( null !== $input->status ) {
- $wc_product->set_status( $input->status->value );
- }
-
- if ( null !== $input->dimensions ) {
- foreach ( array( 'length', 'width', 'height', 'weight' ) as $field ) {
- if ( null !== $input->dimensions->$field ) {
- $wc_product->{"set_{$field}"}( (string) $input->dimensions->$field );
- }
- }
- }
-
- $this->repository->save( $wc_product );
-
- return ProductMapper::from_wc_product( $wc_product );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Products/DeleteProduct.php b/plugins/woocommerce/src/Api/Mutations/Products/DeleteProduct.php
deleted file mode 100644
index 270d238c2d3..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Products/DeleteProduct.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Mutation to delete a product.
- *
- * Demonstrates: mutation returning bool.
- */
-#[Description( 'Delete a product.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-class DeleteProduct {
- /**
- * Execute the mutation.
- *
- * @param int $id The product ID.
- * @param bool $force Whether to permanently delete (bypass trash).
- * @return bool Whether the product was deleted.
- * @throws ApiException When the product is not found.
- */
- public function execute(
- #[Description( 'The ID of the product to delete.' )]
- int $id,
- #[Description( 'Whether to permanently delete the product (bypass trash).' )]
- bool $force = false,
- ): bool {
- $wc_product = wc_get_product( $id );
-
- if ( ! $wc_product instanceof \WC_Product ) {
- throw new ApiException( 'Product not found.', 'NOT_FOUND', status_code: 404 );
- }
-
- // Capture the raw return value. A `(bool)` cast would coerce
- // filter-originated `WP_Error` objects to `true`, reporting failure
- // as success; we need to detect that case explicitly and surface
- // the underlying error instead.
- $deleted = $wc_product->delete( $force );
-
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- if ( $deleted instanceof \WP_Error ) {
- throw new ApiException(
- $deleted->get_error_message(),
- 'INTERNAL_ERROR',
- status_code: 500,
- );
- }
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
-
- return true === $deleted;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Mutations/Products/UpdateProduct.php b/plugins/woocommerce/src/Api/Mutations/Products/UpdateProduct.php
deleted file mode 100644
index 0ae5880949d..00000000000
--- a/plugins/woocommerce/src/Api/Mutations/Products/UpdateProduct.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ReturnType;
-use Automattic\WooCommerce\Api\InputTypes\Products\UpdateProductInput;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-use Automattic\WooCommerce\Api\Utils\Products\ProductMapper;
-
-/**
- * Mutation to update an existing product.
- */
-#[Description( 'Update an existing product.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-class UpdateProduct {
- /**
- * Execute the mutation.
- *
- * @param UpdateProductInput $input The fields to update.
- * @return object
- * @throws ApiException When the product is not found.
- */
- #[ReturnType( Product::class )]
- public function execute(
- #[Description( 'The fields to update.' )]
- UpdateProductInput $input,
- ): object {
- $wc_product = wc_get_product( $input->id );
-
- if ( ! $wc_product instanceof \WC_Product ) {
- throw new ApiException( 'Product not found.', 'NOT_FOUND', status_code: 404 );
- }
-
- foreach ( array( 'name', 'slug', 'sku', 'description', 'short_description', 'manage_stock', 'stock_quantity' ) as $field ) {
- if ( $input->was_provided( $field ) ) {
- $wc_product->{"set_{$field}"}( $input->$field );
- }
- }
-
- foreach ( array( 'regular_price', 'sale_price' ) as $field ) {
- if ( $input->was_provided( $field ) ) {
- $wc_product->{"set_{$field}"}( null !== $input->$field ? (string) $input->$field : '' );
- }
- }
-
- // Nullable enum: only invoke the setter when the client supplied a
- // non-null value. An explicit null means "ignore this field" here —
- // WC_Product's set_status doesn't accept null and would fall back
- // to a default, silently overwriting whatever is already on the
- // product.
- if ( $input->was_provided( 'status' ) && null !== $input->status ) {
- $wc_product->set_status( $input->status->value );
- }
-
- if ( $input->was_provided( 'dimensions' ) ) {
- foreach ( array( 'length', 'width', 'height', 'weight' ) as $field ) {
- if ( $input->dimensions->was_provided( $field ) ) {
- $wc_product->{"set_{$field}"}( null !== $input->dimensions->$field ? (string) $input->dimensions->$field : '' );
- }
- }
- }
-
- $wc_product->save();
-
- return ProductMapper::from_wc_product( $wc_product );
- }
-}
diff --git a/plugins/woocommerce/src/Api/NotFoundException.php b/plugins/woocommerce/src/Api/NotFoundException.php
deleted file mode 100644
index fdcc4488fd8..00000000000
--- a/plugins/woocommerce/src/Api/NotFoundException.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Thrown to signal that the requested resource doesn't exist.
- *
- * Note: when the existence of a resource is itself sensitive (e.g. an order
- * the caller has no business knowing about), prefer {@see UnauthorizedException}
- * instead: leaking a 404 vs 401 distinction lets callers probe for resource
- * existence.
- *
- * Wire shape: `extensions.code = 'NOT_FOUND'`, HTTP status 404.
- */
-class NotFoundException extends ApiException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param array $extensions Additional error metadata to surface in the GraphQL `extensions` object.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message = 'Resource not found.',
- array $extensions = array(),
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, 'NOT_FOUND', $extensions, 404, $previous );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Pagination/Connection.php b/plugins/woocommerce/src/Api/Pagination/Connection.php
deleted file mode 100644
index de81735d929..00000000000
--- a/plugins/woocommerce/src/Api/Pagination/Connection.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Pagination;
-
-/**
- * Represents a Relay-style paginated connection.
- */
-class Connection {
- /**
- * Connection edges wrapping each node with its cursor.
- *
- * @var Edge[]
- */
- public array $edges;
-
- /**
- * The raw nodes without cursor wrappers.
- *
- * @var object[]
- */
- public array $nodes;
-
- public PageInfo $page_info;
-
- public int $total_count;
-
- /**
- * Whether this connection has already been sliced.
- *
- * When true, subsequent calls to slice() return $this immediately,
- * preventing double-slicing when both the command class and the
- * auto-generated resolver call slice().
- *
- * @var bool
- */
- private bool $sliced = false;
-
- /**
- * Create a pre-sliced connection for the performance path.
- *
- * Use this when the DB query already applied pagination limits,
- * so no further slicing is needed.
- *
- * @param Edge[] $edges The already-paginated edges.
- * @param PageInfo $page_info The pagination info.
- * @param int $total_count The total count before pagination.
- * @return self A Connection marked as already sliced.
- */
- public static function pre_sliced( array $edges, PageInfo $page_info, int $total_count ): self {
- $connection = new self();
- $connection->edges = $edges;
- $connection->nodes = array_map( fn( Edge $e ) => $e->node, $edges );
- $connection->page_info = $page_info;
- $connection->total_count = $total_count;
- $connection->sliced = true;
-
- return $connection;
- }
-
- /**
- * Return a new Connection sliced according to the given pagination args.
- *
- * Applies the Relay cursor-based pagination algorithm: first narrow by
- * after/before cursors, then take first N or last N from the remainder.
- *
- * @param array $args Pagination arguments with keys: first, last, after, before.
- * @return self A new Connection with sliced edges/nodes and updated page_info.
- */
- public function slice( array $args ): self {
- if ( $this->sliced ) {
- return $this;
- }
-
- // Enforce the same 0..MAX_PAGE_SIZE bounds that PaginationParams
- // applies to root queries. Without this, nested connection fields
- // (e.g. `variations(first: 1000)`) would slip past the cap because
- // the generated resolver passes raw GraphQL args straight in.
- PaginationParams::validate_args( $args );
-
- $first = $args['first'] ?? null;
- $last = $args['last'] ?? null;
- $after = $args['after'] ?? null;
- $before = $args['before'] ?? null;
-
- // No pagination requested — return as-is.
- if ( null === $first && null === $last && null === $after && null === $before ) {
- return $this;
- }
-
- $edges = $this->edges;
-
- // Narrow by "after" cursor.
- if ( null !== $after ) {
- $found = false;
- foreach ( $edges as $i => $edge ) {
- if ( $edge->cursor === $after ) {
- $edges = array_slice( $edges, $i + 1 );
- $found = true;
- break;
- }
- }
- if ( ! $found ) {
- $edges = array();
- }
- }
-
- // Narrow by "before" cursor.
- if ( null !== $before ) {
- $filtered = array();
- foreach ( $edges as $edge ) {
- if ( $edge->cursor === $before ) {
- break;
- }
- $filtered[] = $edge;
- }
- $edges = $filtered;
- }
-
- $total_after_cursors = count( $edges );
-
- // Apply first/last.
- if ( null !== $first && $first >= 0 ) {
- $edges = array_slice( $edges, 0, $first );
- }
- if ( null !== $last && $last >= 0 ) {
- $edges = array_slice( $edges, max( 0, count( $edges ) - $last ) );
- }
-
- // Build the sliced connection.
- $connection = new self();
- $connection->edges = array_values( $edges );
- $connection->nodes = array_map( fn( Edge $e ) => $e->node, $edges );
- $connection->total_count = $this->total_count;
- $connection->sliced = true;
-
- $page_info = new PageInfo();
- $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
- $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
- $page_info->has_next_page = null !== $first ? count( $edges ) < $total_after_cursors : $this->page_info->has_next_page;
- $page_info->has_previous_page = null !== $last ? count( $edges ) < $total_after_cursors : ( null !== $after );
- $connection->page_info = $page_info;
-
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Pagination/Edge.php b/plugins/woocommerce/src/Api/Pagination/Edge.php
deleted file mode 100644
index 8141f707aa3..00000000000
--- a/plugins/woocommerce/src/Api/Pagination/Edge.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Pagination;
-
-/**
- * Represents an edge in a Relay-style connection.
- */
-class Edge {
- public string $cursor;
-
- public object $node;
-}
diff --git a/plugins/woocommerce/src/Api/Pagination/IdCursorFilter.php b/plugins/woocommerce/src/Api/Pagination/IdCursorFilter.php
deleted file mode 100644
index c543a1e918d..00000000000
--- a/plugins/woocommerce/src/Api/Pagination/IdCursorFilter.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Pagination;
-
-use Automattic\WooCommerce\Api\ApiException;
-
-/**
- * WP_Query ID-cursor pagination helper.
- *
- * Implements cursor-based pagination on the posts ID column by hooking
- * `posts_where` and reading two custom query vars:
- *
- * - `wc_api_after_id` — emit `AND ID > X` in the SQL WHERE clause.
- * - `wc_api_before_id` — emit `AND ID < X`.
- *
- * Resolvers set whichever of those vars they need on their WP_Query args
- * and call {@see self::ensure_registered()} once before running the query.
- * The filter registers itself lazily on first use and short-circuits for
- * any query that doesn't set these vars, so it's safe to leave in place
- * for the rest of the request.
- */
-class IdCursorFilter {
-
- /**
- * Query var for the exclusive lower-bound ID (`ID > X`).
- */
- public const AFTER_ID = 'wc_api_after_id';
-
- /**
- * Query var for the exclusive upper-bound ID (`ID < X`).
- */
- public const BEFORE_ID = 'wc_api_before_id';
-
- /**
- * Whether the posts_where hook is currently registered for this request.
- *
- * @var bool
- */
- private static bool $registered = false;
-
- /**
- * Register the posts_where filter on first call; no-op thereafter.
- *
- * The filter is a no-op for queries that don't set the cursor query
- * vars, so leaving it registered for the remainder of the request is
- * harmless — and it means resolvers never need to clean up after
- * themselves, which is how the previous add/remove dance leaked.
- */
- public static function ensure_registered(): void {
- if ( self::$registered ) {
- return;
- }
- add_filter( 'posts_where', array( self::class, 'apply' ), 10, 2 );
- self::$registered = true;
- }
-
- /**
- * Filter callback for `posts_where`. Appends cursor conditions when the
- * corresponding query vars are set on the WP_Query; returns the input
- * clause unchanged otherwise.
- *
- * @param string $where SQL WHERE clause being built.
- * @param \WP_Query $query The WP_Query being prepared.
- * @return string The modified WHERE clause.
- */
- public static function apply( string $where, \WP_Query $query ): string {
- $after = (int) $query->get( self::AFTER_ID );
- $before = (int) $query->get( self::BEFORE_ID );
-
- if ( $after <= 0 && $before <= 0 ) {
- return $where;
- }
-
- global $wpdb;
- if ( $after > 0 ) {
- $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $after );
- }
- if ( $before > 0 ) {
- $where .= $wpdb->prepare( " AND {$wpdb->posts}.ID < %d", $before );
- }
- return $where;
- }
-
- /**
- * Decode a base64-encoded ID cursor into a positive integer.
- *
- * Resolvers encode cursors via `base64_encode( (string) $id )` on the
- * way out; this is the symmetric decode. `base64_decode(..., true)`
- * returns false for malformed input, which `(int)` casts to 0 and
- * {@see self::apply()} would silently treat as "no cursor" — leaving
- * clients with unfiltered results instead of a clear error. Validate
- * explicitly and throw INVALID_ARGUMENT → HTTP 400 on any bad input.
- *
- * @param string $cursor The client-supplied cursor string.
- * @param string $name Which cursor argument (`after` / `before`), for error messages.
- * @return int The decoded positive integer ID.
- * @throws ApiException When the cursor isn't a valid base64-encoded positive integer.
- */
- public static function decode_id_cursor( string $cursor, string $name ): int {
- $raw = base64_decode( $cursor, true );
- if ( false === $raw || ! ctype_digit( $raw ) || (int) $raw <= 0 ) {
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- throw new ApiException(
- sprintf( 'Invalid `%s` cursor.', $name ),
- 'INVALID_ARGUMENT',
- status_code: 400,
- );
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
- return (int) $raw;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Pagination/PageInfo.php b/plugins/woocommerce/src/Api/Pagination/PageInfo.php
deleted file mode 100644
index e69419aa897..00000000000
--- a/plugins/woocommerce/src/Api/Pagination/PageInfo.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Pagination;
-
-/**
- * Pagination metadata for a connection.
- */
-class PageInfo {
- public bool $has_next_page;
-
- public bool $has_previous_page;
-
- public ?string $start_cursor;
-
- public ?string $end_cursor;
-}
diff --git a/plugins/woocommerce/src/Api/Pagination/PaginationParams.php b/plugins/woocommerce/src/Api/Pagination/PaginationParams.php
deleted file mode 100644
index 6391e5ce6c9..00000000000
--- a/plugins/woocommerce/src/Api/Pagination/PaginationParams.php
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Pagination;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Unroll;
-
-/**
- * Standard pagination parameters for connection queries.
- *
- * Because this class carries #[Unroll], whenever it is used as an execute()
- * parameter the builder expands its properties into individual GraphQL arguments.
- */
-#[Unroll]
-class PaginationParams {
- /**
- * Maximum number of items a client may request in a single page.
- *
- * Requests with `first` or `last` above this value are rejected with an
- * INVALID_ARGUMENT error, matching the behavior of common GraphQL APIs
- * (e.g. GitHub's 100-item cap).
- */
- public const MAX_PAGE_SIZE = 100;
-
- /**
- * Page size used when neither `first` nor `last` is provided.
- */
- public const DEFAULT_PAGE_SIZE = 100;
-
- /**
- * Constructor.
- *
- * @param ?int $first Return the first N results.
- * @param ?int $last Return the last N results.
- * @param ?string $after Return results after this cursor.
- * @param ?string $before Return results before this cursor.
- *
- * @throws \InvalidArgumentException When `first` or `last` is negative or exceeds MAX_PAGE_SIZE.
- */
- public function __construct(
- #[Description( 'Return the first N results. Must be between 0 and ' . self::MAX_PAGE_SIZE . '.' )]
- public readonly ?int $first = null,
- #[Description( 'Return the last N results. Must be between 0 and ' . self::MAX_PAGE_SIZE . '.' )]
- public readonly ?int $last = null,
- #[Description( 'Return results after this cursor.' )]
- public readonly ?string $after = null,
- #[Description( 'Return results before this cursor.' )]
- public readonly ?string $before = null,
- ) {
- self::validate_limit( 'first', $first );
- self::validate_limit( 'last', $last );
- }
-
- /**
- * The page size to use when no explicit `first` or `last` is provided.
- *
- * Exposed as a method (not just the constant) so the default can become
- * configurable — e.g. via a filter or store option — without requiring
- * call-site changes.
- *
- * @return int
- */
- public static function get_default_page_size(): int {
- return self::DEFAULT_PAGE_SIZE;
- }
-
- /**
- * Validate pagination limits on a raw args array without constructing a
- * full PaginationParams instance.
- *
- * Intended for call sites that take raw GraphQL args (like nested
- * connection resolvers) and forward them to Connection::slice(). The
- * constructor already runs the same checks for root queries that build
- * a PaginationParams via #[Unroll], so this keeps both paths in sync.
- *
- * @param array $args Raw args with optional `first` / `last` keys.
- *
- * @throws \InvalidArgumentException When either limit is negative or above MAX_PAGE_SIZE.
- */
- public static function validate_args( array $args ): void {
- self::validate_limit( 'first', $args['first'] ?? null );
- self::validate_limit( 'last', $args['last'] ?? null );
- }
-
- /**
- * Validate a `first` / `last` argument.
- *
- * @param string $name The argument name, for the error message.
- * @param ?int $value The value to validate.
- *
- * @throws \InvalidArgumentException When the value is out of range.
- */
- private static function validate_limit( string $name, ?int $value ): void {
- if ( null === $value ) {
- return;
- }
-
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML output; serialized as JSON in the GraphQL error response.
- if ( $value < 0 ) {
- throw new \InvalidArgumentException(
- sprintf( 'Argument `%s` must be zero or greater.', $name )
- );
- }
-
- if ( $value > self::MAX_PAGE_SIZE ) {
- throw new \InvalidArgumentException(
- sprintf(
- 'Argument `%s` exceeds the maximum page size of %d.',
- $name,
- self::MAX_PAGE_SIZE
- )
- );
- }
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
-}
diff --git a/plugins/woocommerce/src/Api/Queries/Coupons/GetCoupon.php b/plugins/woocommerce/src/Api/Queries/Coupons/GetCoupon.php
deleted file mode 100644
index 4f011350ec5..00000000000
--- a/plugins/woocommerce/src/Api/Queries/Coupons/GetCoupon.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Queries\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Types\Coupons\Coupon;
-use Automattic\WooCommerce\Api\Utils\Coupons\CouponMapper;
-
-#[Name( 'coupon' )]
-#[Description( 'Retrieve a single coupon by ID or code. Exactly one of the two arguments must be provided.' )]
-/**
- * Query to retrieve a single coupon.
- */
-#[RequiredCapability( 'read_private_shop_coupons' )]
-class GetCoupon {
- /**
- * Retrieve a coupon by ID or code.
- *
- * @param ?int $id The coupon ID.
- * @param ?string $code The coupon code.
- * @return ?Coupon
- * @throws \InvalidArgumentException When neither or both arguments are provided.
- */
- 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 {
- if ( ( null === $id ) === ( null === $code ) ) {
- throw new \InvalidArgumentException( 'Exactly one of "id" or "code" must be provided.' );
- }
-
- $wc_coupon = new \WC_Coupon( $id ?? $code );
-
- if ( ! $wc_coupon->get_id() ) {
- return null;
- }
-
- return CouponMapper::from_wc_coupon( $wc_coupon );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Queries/Coupons/ListCoupons.php b/plugins/woocommerce/src/Api/Queries/Coupons/ListCoupons.php
deleted file mode 100644
index c1f6cc5a87e..00000000000
--- a/plugins/woocommerce/src/Api/Queries/Coupons/ListCoupons.php
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Queries\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\Edge;
-use Automattic\WooCommerce\Api\Pagination\IdCursorFilter;
-use Automattic\WooCommerce\Api\Pagination\PageInfo;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Api\Types\Coupons\Coupon;
-use Automattic\WooCommerce\Api\Utils\Coupons\CouponMapper;
-
-#[Name( 'coupons' )]
-#[Description( 'List coupons with cursor-based pagination.' )]
-/**
- * Query to list coupons with cursor-based pagination.
- */
-#[RequiredCapability( 'read_private_shop_coupons' )]
-class ListCoupons {
- /**
- * List coupons with optional filtering and pagination.
- *
- * @param PaginationParams $pagination The pagination parameters.
- * @param ?CouponStatus $status Optional status filter.
- * @return Connection
- */
- #[ConnectionOf( Coupon::class )]
- public function execute(
- PaginationParams $pagination,
- #[Description( 'Filter by coupon status.' )]
- ?CouponStatus $status = null,
- ): Connection {
- $first = $pagination->first;
- $last = $pagination->last;
- $after = $pagination->after;
- $before = $pagination->before;
- $limit = $first ?? $last ?? PaginationParams::get_default_page_size();
-
- // Use WP_Query for the count and a filtered query for cursor-based
- // pagination. We only need `found_posts` (which comes from the
- // SQL_CALC_FOUND_ROWS query WP runs alongside the main SELECT), so
- // the main SELECT fetches only one row — posts_per_page => -1 would
- // materialize every ID just to throw it away.
- $count_args = array(
- 'post_type' => 'shop_coupon',
- 'posts_per_page' => 1,
- 'fields' => 'ids',
- 'post_status' => $status?->value ?? 'any',
- );
- $count_query = new \WP_Query( $count_args );
- $total_count = $count_query->found_posts;
-
- // Fetch posts with cursor filtering via post__in or meta_query workaround.
- // For simplicity, we use direct ID-based filtering.
- $posts_query_args = array(
- 'post_type' => 'shop_coupon',
- 'posts_per_page' => $limit + 1,
- 'orderby' => 'ID',
- 'order' => null !== $last ? 'DESC' : 'ASC',
- 'post_status' => $status?->value ?? 'any',
- );
-
- if ( null !== $after ) {
- $posts_query_args[ IdCursorFilter::AFTER_ID ] = IdCursorFilter::decode_id_cursor( $after, 'after' );
- }
- if ( null !== $before ) {
- $posts_query_args[ IdCursorFilter::BEFORE_ID ] = IdCursorFilter::decode_id_cursor( $before, 'before' );
- }
- IdCursorFilter::ensure_registered();
-
- $query = new \WP_Query( $posts_query_args );
- $posts = $query->posts;
-
- // Determine pagination.
- $has_extra = count( $posts ) > $limit;
- if ( $has_extra ) {
- $posts = array_slice( $posts, 0, $limit );
- }
-
- // If we fetched in DESC order for $last, reverse to get ascending order.
- if ( null !== $last ) {
- $posts = array_reverse( $posts );
- }
-
- // Build edges and nodes.
- $edges = array();
- $nodes = array();
- foreach ( $posts as $post ) {
- $wc_coupon = new \WC_Coupon( $post->ID );
- $coupon = CouponMapper::from_wc_coupon( $wc_coupon );
-
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $coupon->id );
- $edge->node = $coupon;
-
- $edges[] = $edge;
- $nodes[] = $coupon;
- }
-
- $page_info = new PageInfo();
- // Relay semantics for backward pagination (`last`, `before`): the
- // returned window ends just before `$before`, so items after the
- // window exist whenever `$before` was supplied — not whenever
- // `$after` was. `has_previous_page` in the backward case is driven
- // by the "did we fetch limit+1?" sentinel (`$has_extra`).
- $page_info->has_next_page = null !== $last ? ( null !== $before ) : $has_extra;
- $page_info->has_previous_page = null !== $last ? $has_extra : ( null !== $after );
- $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
- $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
-
- $connection = new Connection();
- $connection->edges = $edges;
- $connection->nodes = $nodes;
- $connection->page_info = $page_info;
- $connection->total_count = $total_count;
-
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Queries/Products/GetProduct.php b/plugins/woocommerce/src/Api/Queries/Products/GetProduct.php
deleted file mode 100644
index 79626cafd26..00000000000
--- a/plugins/woocommerce/src/Api/Queries/Products/GetProduct.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Queries\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ReturnType;
-use Automattic\WooCommerce\Api\UnauthorizedException;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-use Automattic\WooCommerce\Api\Utils\Products\ProductMapper;
-
-/**
- * Query to retrieve a single product by ID.
- *
- * Demonstrates: authorize(), $_query_info, UnauthorizedException.
- *
- * Authorization logic: admins (manage_woocommerce) can read any product,
- * non-admin users can only read their own products.
- */
-#[Name( 'product' )]
-#[Description( 'Retrieve a single product by ID.' )]
-#[RequiredCapability( 'read_product' )]
-class GetProduct {
- /**
- * Authorize access to a specific product.
- *
- * Admins can read any product. Non-admin users can only read products
- * they authored themselves.
- *
- * Every inaccessible case throws `UnauthorizedException('Product not
- * found.')` — whether the ID doesn't exist, points at a non-product
- * post type, or points at a product the caller doesn't own. This
- * prevents callers from enumerating product IDs vs non-product post
- * IDs via the response they get back (which would otherwise be "not
- * found" vs "no permission").
- *
- * @param int $id The product ID.
- * @param bool $_preauthorized Whether the declared capability check passed.
- * @return bool Whether the current user can read this product.
- * @throws UnauthorizedException When the product is not accessible.
- */
- public function authorize( int $id, bool $_preauthorized ): bool {
- // Reject non-positive IDs up front. `get_post( 0 )` inside a
- // WordPress loop returns `$GLOBALS['post']` (not null), so a bare
- // `get_post( $id )` below would accidentally operate on whatever
- // global post was set upstream of this request.
- if ( $id <= 0 ) {
- throw new UnauthorizedException( 'Product not found.' );
- }
-
- $post = get_post( $id );
-
- if ( ! $post || 'product' !== $post->post_type ) {
- throw new UnauthorizedException( 'Product not found.' );
- }
-
- // Honor the declared #[RequiredCapability] (read_product).
- if ( $_preauthorized ) {
- return true;
- }
-
- // `manage_woocommerce` is the canonical "admin sees everything"
- // capability in WooCommerce. The declared #[RequiredCapability]
- // pre-authorizes on `read_product` (the read-level post-type cap,
- // which is what the schema advertises), but an admin whose cap set
- // grants `manage_woocommerce` without `read_product` would
- // otherwise fall through to the ownership check and get "Product
- // not found" for any product they don't own — contrary to the
- // documented admin-can-see-everything contract.
- if ( current_user_can( 'manage_woocommerce' ) ) {
- return true;
- }
-
- // Non-admin users can only read their own products. Throw the same
- // "not found" exception rather than returning false — a distinct
- // "you don't have permission" error here would tell the caller
- // that the ID is a product (just not theirs), leaking the
- // product-ID space vs the rest of the post-ID space.
- //
- // Reject guest users explicitly: get_current_user_id() returns 0
- // for unauthenticated callers, and products created via WP-CLI,
- // imports, or programmatic inserts without an author can have
- // post_author = 0 — a bare `!==` check would mis-grant access to
- // anonymous callers for those products.
- $current_user_id = get_current_user_id();
- if ( 0 === $current_user_id || $current_user_id !== (int) $post->post_author ) {
- throw new UnauthorizedException( 'Product not found.' );
- }
-
- return true;
- }
-
- /**
- * Retrieve a product by ID.
- *
- * @param int $id The product ID.
- * @param ?array $_query_info Unified query info tree from the GraphQL request.
- * @return ?object
- */
- #[ReturnType( Product::class )]
- public function execute(
- #[Description( 'The ID of the product to retrieve.' )]
- int $id,
- ?array $_query_info = null,
- ): ?object {
- // Mirrors the guard in authorize(): never pass a non-positive ID to
- // wc_get_product(). authorize() would normally reject these first,
- // but a future caller path might invoke execute() directly.
- if ( $id <= 0 ) {
- return null;
- }
-
- $wc_product = wc_get_product( $id );
-
- if ( ! $wc_product instanceof \WC_Product ) {
- return null;
- }
-
- return ProductMapper::from_wc_product( $wc_product, $_query_info );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Queries/Products/ListProducts.php b/plugins/woocommerce/src/Api/Queries/Products/ListProducts.php
deleted file mode 100644
index b67849e8d88..00000000000
--- a/plugins/woocommerce/src/Api/Queries/Products/ListProducts.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Queries\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\Unroll;
-use Automattic\WooCommerce\Api\Enums\Products\ProductType;
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus;
-use Automattic\WooCommerce\Api\InputTypes\Products\ProductFilterInput;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\Edge;
-use Automattic\WooCommerce\Api\Pagination\IdCursorFilter;
-use Automattic\WooCommerce\Api\Pagination\PageInfo;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-use Automattic\WooCommerce\Api\Utils\Products\ProductMapper;
-
-/**
- * Query to list products with cursor-based pagination.
- *
- * Demonstrates: #[Unroll] on parameter, enum as direct param, multiple capabilities.
- */
-#[Name( 'products' )]
-#[Description( 'List products with cursor-based pagination and optional filtering.' )]
-#[RequiredCapability( 'manage_woocommerce' )]
-#[RequiredCapability( 'edit_products' )]
-class ListProducts {
- /**
- * List products with optional filtering and pagination.
- *
- * @param PaginationParams $pagination The pagination parameters.
- * @param ProductFilterInput $filters Filter criteria (unrolled to flat args).
- * @param ?ProductType $product_type Optional product type filter.
- * @param ?array $_query_info Unified query info tree from the GraphQL request.
- * @return Connection
- * @throws ApiException When an unsupported `stock_status` filter value is passed.
- */
- #[ConnectionOf( Product::class )]
- public function execute(
- PaginationParams $pagination,
- #[Unroll]
- ProductFilterInput $filters,
- #[Description( 'Filter by product type.' )]
- ?ProductType $product_type = null,
- ?array $_query_info = null,
- ): Connection {
- $first = $pagination->first;
- $last = $pagination->last;
- $after = $pagination->after;
- $before = $pagination->before;
- $limit = $first ?? $last ?? PaginationParams::get_default_page_size();
-
- $query_args = array(
- 'post_type' => 'product',
- 'posts_per_page' => $limit + 1,
- 'orderby' => 'ID',
- 'order' => null !== $last ? 'DESC' : 'ASC',
- 'post_status' => $filters->status?->value ?? 'any',
- );
-
- // Product type filter via taxonomy. `ProductType::Other` is the
- // output-only signal for "stored product_type doesn't match any
- // known standard" (typically plugin-added types), mirroring how
- // `StockStatus::Other` is handled for the meta-query path above.
- // Map it to NOT IN the standard slugs rather than the literal
- // 'other' term, which wouldn't match anything.
- if ( null !== $product_type ) {
- if ( ProductType::Other === $product_type ) {
- $query_args['tax_query'] = array(
- array(
- 'taxonomy' => 'product_type',
- 'field' => 'slug',
- 'terms' => array_values(
- array_filter(
- array_map(
- static fn( ProductType $t ): string => $t->value,
- ProductType::cases()
- ),
- static fn( string $slug ): bool => ProductType::Other->value !== $slug
- )
- ),
- 'operator' => 'NOT IN',
- ),
- );
- } else {
- $query_args['tax_query'] = array(
- array(
- 'taxonomy' => 'product_type',
- 'field' => 'slug',
- 'terms' => $product_type->value,
- ),
- );
- }
- }
-
- // Stock status filter via meta. `StockStatus::Other` means "stored
- // _stock_status isn't one of the three standard WooCommerce values"
- // (typically a plugin-added custom status), so it maps to NOT IN
- // those three. `default` throws INVALID_ARGUMENT so any future
- // enum case added without updating this match fails loudly with a
- // clean 400 instead of a PHP-level UnhandledMatchError → HTTP 500.
- if ( null !== $filters->stock_status ) {
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
- $meta_clause = match ( $filters->stock_status ) {
- StockStatus::InStock => array(
- 'key' => '_stock_status',
- 'value' => 'instock',
- ),
- StockStatus::OutOfStock => array(
- 'key' => '_stock_status',
- 'value' => 'outofstock',
- ),
- StockStatus::OnBackorder => array(
- 'key' => '_stock_status',
- 'value' => 'onbackorder',
- ),
- StockStatus::Other => array(
- 'key' => '_stock_status',
- 'value' => array( 'instock', 'outofstock', 'onbackorder' ),
- 'compare' => 'NOT IN',
- ),
- default => throw new ApiException(
- sprintf( 'Unsupported stock_status filter value: %s.', $filters->stock_status->name ),
- 'INVALID_ARGUMENT',
- status_code: 400,
- ),
- };
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- $query_args['meta_query'] = array( $meta_clause );
- }
-
- // Search filter.
- if ( null !== $filters->search ) {
- $query_args['s'] = $filters->search;
- }
-
- // Total count query. Derive from $query_args — which already has
- // the tax_query / meta_query / search clauses applied — *before*
- // we set cursor query vars on it. Building $count_args from scratch
- // with only post_status would drop every user filter and report the
- // count of "all products in that status" instead of "all products
- // matching the filters", making Relay consumers' "X of Y" wrong.
- // Only `found_posts` is read, so posts_per_page => 1 keeps the
- // underlying SELECT cheap.
- $count_args = $query_args;
- $count_args['posts_per_page'] = 1;
- $count_args['fields'] = 'ids';
- $count_query = new \WP_Query( $count_args );
- $total_count = $count_query->found_posts;
-
- // Cursor-based filtering via IdCursorFilter (see class docblock).
- if ( null !== $after ) {
- $query_args[ IdCursorFilter::AFTER_ID ] = IdCursorFilter::decode_id_cursor( $after, 'after' );
- }
- if ( null !== $before ) {
- $query_args[ IdCursorFilter::BEFORE_ID ] = IdCursorFilter::decode_id_cursor( $before, 'before' );
- }
- IdCursorFilter::ensure_registered();
-
- $query = new \WP_Query( $query_args );
- $posts = $query->posts;
-
- // Determine pagination.
- $has_extra = count( $posts ) > $limit;
- if ( $has_extra ) {
- $posts = array_slice( $posts, 0, $limit );
- }
-
- if ( null !== $last ) {
- $posts = array_reverse( $posts );
- }
-
- // Narrow $_query_info to the per-node selection so each mapped
- // product only fetches the subtrees the client actually asked for
- // under `nodes { ... }` / `edges { node { ... } }`. Without this,
- // ProductMapper::populate_common_fields() hits its null-$query_info
- // fallback and runs build_reviews() (plus its count query) for
- // every product on the page — N+1 on reviews even when no client
- // selected them.
- $node_query_info = ProductMapper::connection_node_info( $_query_info );
-
- // Build edges and nodes.
- $edges = array();
- $nodes = array();
- foreach ( $posts as $post ) {
- $wc_product = wc_get_product( $post->ID );
- if ( ! $wc_product instanceof \WC_Product ) {
- continue;
- }
-
- $product = ProductMapper::from_wc_product( $wc_product, $node_query_info );
-
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $product->id );
- $edge->node = $product;
-
- $edges[] = $edge;
- $nodes[] = $product;
- }
-
- $page_info = new PageInfo();
- // Relay semantics for backward pagination (`last`, `before`): the
- // returned window ends just before `$before`, so items after the
- // window exist whenever `$before` was supplied — not whenever
- // `$after` was. `has_previous_page` in the backward case is driven
- // by the "did we fetch limit+1?" sentinel (`$has_extra`).
- $page_info->has_next_page = null !== $last ? ( null !== $before ) : $has_extra;
- $page_info->has_previous_page = null !== $last ? $has_extra : ( null !== $after );
- $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
- $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
-
- $connection = new Connection();
- $connection->edges = $edges;
- $connection->nodes = $nodes;
- $connection->page_info = $page_info;
- $connection->total_count = $total_count;
-
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/src/Api/README.md b/plugins/woocommerce/src/Api/README.md
deleted file mode 100644
index 4ebbad72333..00000000000
--- a/plugins/woocommerce/src/Api/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Important: Experimental feature
-
-All the code in this directory (`Automattic\WooCommerce\Api` namespace and nested namespaces) is part of [an experimental feature](https://github.com/woocommerce/woocommerce/pull/63772). The code could (and probably will) get backwards-incompatible changes, or even be completely removed, in future releases of WooCommerce.
-
-Feel free to experiment in testing or staging environments, but **DO NOT** use this code in released extensions or in production environments.
-
-Also as a reminder, **ALL** the code that's inside the `Automattic\WooCommerce\Internal` namespace and nested namespaces, or that's annotated with `@internal`, is for exclusive usage of WooCommerce core and must **NEVER** be used in extensions or otherwise in production environments.
-
-## Where the autogenerated-callable runtime code lives
-
-The `Infrastructure/` subdirectory holds the classes that the autogenerated GraphQL code emitted by `pnpm build:api` references at runtime:
-
-- `Infrastructure\GraphQLControllerBase`: abstract base extended by each generated `GraphQLController` subclass.
-- `Infrastructure\ResolverHelpers`: exception translation, pagination construction, and authorization helpers called from generated resolvers.
-- `Infrastructure\QueryInfoExtractor`: turns the engine's `ResolveInfo` into the `_query_info` tree resolvers expose to commands.
-- `Infrastructure\MetadataController`: contributes the `_apiMetadata` root query field that every generated schema inherits.
-- `Infrastructure\Schema\*`: the engine-decoupled schema surface (see `Infrastructure/Schema/README.md`); every type/resolver/root-type class in a generated tree imports from here.
-- `Infrastructure\{ClassResolver, Principal, PrincipalResolver}`: convention classes detected at build time and wired into the generated controller.
-
-The public signatures on these classes use only `Api\*` types (and `Schema\*` for any engine-shaped objects), generated trees committed to plugin repos will stay compilable in the event of WooCommerce switching to a different underlying GraphQL engine in the future.
diff --git a/plugins/woocommerce/src/Api/Scalars/DateTime.php b/plugins/woocommerce/src/Api/Scalars/DateTime.php
deleted file mode 100644
index a393643e22b..00000000000
--- a/plugins/woocommerce/src/Api/Scalars/DateTime.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Scalars;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Custom scalar for ISO 8601 date/time values.
- */
-#[Description( 'An ISO 8601 encoded date and time string.' )]
-class DateTime {
- /**
- * Serialize a PHP value to the scalar's transport format.
- *
- * @param mixed $value The value to serialize.
- * @return string
- */
- public static function serialize( mixed $value ): string {
- if ( $value instanceof \DateTimeInterface ) {
- return $value->format( \DateTimeInterface::ATOM );
- }
- return (string) $value;
- }
-
- /**
- * Parse a value received from a client (variable or literal).
- *
- * @param string $value The raw string value from the client.
- * @return \DateTimeImmutable
- * @throws \InvalidArgumentException When the value cannot be parsed as an ISO 8601 date/time string.
- */
- public static function parse( string $value ): \DateTimeImmutable {
- try {
- return new \DateTimeImmutable( $value );
- } catch ( \Exception $e ) {
- // PHP 8.3+ throws \DateMalformedStringException; earlier versions
- // throw a plain \Exception. Both extend \Exception, so a single
- // catch captures them.
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML output; serialized as JSON in the GraphQL error response.
- throw new \InvalidArgumentException(
- sprintf( 'Invalid ISO 8601 date/time: %s', $e->getMessage() ),
- 0,
- $e
- );
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
- }
-}
diff --git a/plugins/woocommerce/src/Api/Traits/RequiresManageWoocommerce.php b/plugins/woocommerce/src/Api/Traits/RequiresManageWoocommerce.php
deleted file mode 100644
index 61d925f4292..00000000000
--- a/plugins/woocommerce/src/Api/Traits/RequiresManageWoocommerce.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Traits;
-
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Trait that grants the manage_woocommerce capability requirement.
- *
- * Classes using this trait inherit the capability via the builder's
- * resolve_capabilities() method, which inspects traits for attributes.
- */
-#[RequiredCapability( 'manage_woocommerce' )]
-trait RequiresManageWoocommerce {
-}
diff --git a/plugins/woocommerce/src/Api/Types/Coupons/Coupon.php b/plugins/woocommerce/src/Api/Types/Coupons/Coupon.php
deleted file mode 100644
index e7cc1fa6d39..00000000000
--- a/plugins/woocommerce/src/Api/Types/Coupons/Coupon.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\Interfaces\ObjectWithId;
-use Automattic\WooCommerce\Api\Scalars\DateTime;
-
-/**
- * Output type representing a WooCommerce coupon.
- */
-#[Description( 'Represents a WooCommerce discount coupon.' )]
-class Coupon {
- use ObjectWithId;
-
- #[Description( 'The coupon code.' )]
- public string $code;
-
- #[Description( 'The coupon description.' )]
- public string $description;
-
- #[Description( 'The type of discount.' )]
- public DiscountType $discount_type;
-
- #[Description( 'The raw discount type as stored in WooCommerce. Useful when discount_type is OTHER (e.g. plugin-added types like recurring_percent or sign_up_fee).' )]
- public string $raw_discount_type;
-
- #[Description( 'The discount amount.' )]
- public float $amount;
-
- #[Description( 'The coupon status.' )]
- public CouponStatus $status;
-
- #[Description( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).' )]
- public string $raw_status;
-
- #[Description( 'The date the coupon was created.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_created;
-
- #[Description( 'The date the coupon was last modified.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_modified;
-
- #[Description( 'The date the coupon expires.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_expires;
-
- #[Description( 'The number of times the coupon has been used.' )]
- public int $usage_count;
-
- #[Description( 'Whether the coupon can only be used alone.' )]
- public bool $individual_use;
-
- #[Description( 'Product IDs the coupon can be applied to.' )]
- #[ArrayOf( 'int' )]
- public array $product_ids;
-
- #[Description( 'Product IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public array $excluded_product_ids;
-
- #[Description( 'Maximum number of times the coupon can be used in total.' )]
- public int $usage_limit;
-
- #[Description( 'Maximum number of times the coupon can be used per customer.' )]
- public int $usage_limit_per_user;
-
- #[Description( 'Maximum number of items the coupon can be applied to.' )]
- public ?int $limit_usage_to_x_items;
-
- #[Description( 'Whether the coupon grants free shipping.' )]
- public bool $free_shipping;
-
- #[Description( 'Product category IDs the coupon applies to.' )]
- #[ArrayOf( 'int' )]
- public array $product_categories;
-
- #[Description( 'Product category IDs excluded from the coupon.' )]
- #[ArrayOf( 'int' )]
- public array $excluded_product_categories;
-
- #[Description( 'Whether the coupon excludes items on sale.' )]
- public bool $exclude_sale_items;
-
- #[Description( 'Minimum order amount required to use the coupon.' )]
- public float $minimum_amount;
-
- #[Description( 'Maximum order amount allowed to use the coupon.' )]
- public float $maximum_amount;
-
- #[Description( 'Email addresses that can use this coupon.' )]
- #[ArrayOf( 'string' )]
- public array $email_restrictions;
-
- #[Description( 'Email addresses of customers who have used this coupon.' )]
- #[ArrayOf( 'string' )]
- public array $used_by;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Coupons/DeleteCouponResult.php b/plugins/woocommerce/src/Api/Types/Coupons/DeleteCouponResult.php
deleted file mode 100644
index e1c75c2048f..00000000000
--- a/plugins/woocommerce/src/Api/Types/Coupons/DeleteCouponResult.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Coupons;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Result of a coupon deletion operation.
- */
-#[Description( 'The result of deleting a coupon.' )]
-class DeleteCouponResult {
- #[Description( 'The ID of the deleted coupon.' )]
- public int $id;
-
- #[Description( 'Whether the coupon was permanently deleted.' )]
- public bool $deleted;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ExternalProduct.php b/plugins/woocommerce/src/Api/Types/Products/ExternalProduct.php
deleted file mode 100644
index 65e0a01c0b8..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ExternalProduct.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-
-/**
- * Output type representing an external/affiliate product.
- */
-#[Description( 'An external/affiliate product.' )]
-class ExternalProduct {
- use Product;
-
- #[Description( 'The external product URL.' )]
- public ?string $product_url;
-
- #[Description( 'The text for the external product button.' )]
- public ?string $button_text;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ProductAttribute.php b/plugins/woocommerce/src/Api/Types/Products/ProductAttribute.php
deleted file mode 100644
index afb3950112a..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ProductAttribute.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Output type representing a product attribute definition.
- */
-#[Description( 'A product attribute.' )]
-class ProductAttribute {
- #[Description( 'The attribute display name.' )]
- public string $name;
-
- #[Description( 'The attribute taxonomy or key name.' )]
- public string $slug;
-
- #[Description( 'The available attribute values.' )]
- #[ArrayOf( 'string' )]
- public array $options;
-
- #[Description( 'The display order position.' )]
- public int $position;
-
- #[Description( 'Whether the attribute is visible on the product page.' )]
- public bool $visible;
-
- #[Description( 'Whether the attribute is used for variations.' )]
- public bool $variation;
-
- #[Description( 'Whether the attribute is a global taxonomy attribute.' )]
- public bool $is_taxonomy;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ProductDimensions.php b/plugins/woocommerce/src/Api/Types/Products/ProductDimensions.php
deleted file mode 100644
index fcef48187c9..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ProductDimensions.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Output type representing product physical dimensions.
- */
-#[Description( 'Physical dimensions and weight of a product.' )]
-class ProductDimensions {
- #[Description( 'The product length.' )]
- public ?float $length;
-
- #[Description( 'The product width.' )]
- public ?float $width;
-
- #[Description( 'The product height.' )]
- public ?float $height;
-
- #[Description( 'The product weight.' )]
- public ?float $weight;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ProductImage.php b/plugins/woocommerce/src/Api/Types/Products/ProductImage.php
deleted file mode 100644
index 9dfae3d203e..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ProductImage.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Output type representing a product image.
- */
-#[Description( 'Represents a product image.' )]
-class ProductImage {
- #[Description( 'The image attachment ID.' )]
- public int $id;
-
- #[Description( 'The image URL.' )]
- public string $url;
-
- #[Description( 'The image alt text.' )]
- public string $alt;
-
- #[Description( 'The image display position.' )]
- public int $position;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ProductReview.php b/plugins/woocommerce/src/Api/Types/Products/ProductReview.php
deleted file mode 100644
index c4e63cb1427..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ProductReview.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\Scalars\DateTime;
-
-/**
- * Output type representing a product review.
- */
-#[Description( 'Represents a customer review for a product.' )]
-class ProductReview {
- #[Description( 'The review ID.' )]
- public int $id;
-
- #[Description( 'The product ID this review belongs to.' )]
- public int $product_id;
-
- #[Description( 'The reviewer name.' )]
- public string $reviewer;
-
- #[Description( 'The review content.' )]
- public string $review;
-
- #[Description( 'The review rating (1-5).' )]
- public int $rating;
-
- #[Description( 'The date the review was created.' )]
- #[ScalarType( DateTime::class )]
- public ?string $date_created;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/ProductVariation.php b/plugins/woocommerce/src/Api/Types/Products/ProductVariation.php
deleted file mode 100644
index dffd11e5961..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/ProductVariation.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-
-/**
- * Output type representing a product variation.
- */
-#[Description( 'A product variation.' )]
-class ProductVariation {
- use Product;
-
- #[Description( 'The parent variable product ID.' )]
- public int $parent_id;
-
- #[Description( 'The selected attribute values for this variation.' )]
- #[ArrayOf( SelectedAttribute::class )]
- public array $selected_attributes;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/SelectedAttribute.php b/plugins/woocommerce/src/Api/Types/Products/SelectedAttribute.php
deleted file mode 100644
index 6c5e3d05d0c..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/SelectedAttribute.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Output type representing a single attribute selection on a variation.
- */
-#[Description( 'A selected attribute value on a product variation.' )]
-class SelectedAttribute {
- #[Description( 'The attribute name or slug.' )]
- public string $name;
-
- #[Description( 'The selected attribute value.' )]
- public string $value;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/SimpleProduct.php b/plugins/woocommerce/src/Api/Types/Products/SimpleProduct.php
deleted file mode 100644
index 0baebc7069b..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/SimpleProduct.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-
-/**
- * Output type representing a simple WooCommerce product.
- */
-#[Description( 'A simple WooCommerce product.' )]
-class SimpleProduct {
- use Product;
-}
diff --git a/plugins/woocommerce/src/Api/Types/Products/VariableProduct.php b/plugins/woocommerce/src/Api/Types/Products/VariableProduct.php
deleted file mode 100644
index 2984efb2143..00000000000
--- a/plugins/woocommerce/src/Api/Types/Products/VariableProduct.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Types\Products;
-
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Parameter;
-use Automattic\WooCommerce\Api\Interfaces\Product;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-
-/**
- * Output type representing a variable product with variations.
- */
-#[Description( 'A variable product with variations.' )]
-class VariableProduct {
- use Product;
-
- #[Description( 'The product variations.' )]
- #[ConnectionOf( ProductVariation::class )]
- #[Parameter( type: PaginationParams::class )]
- public Connection $variations;
-}
diff --git a/plugins/woocommerce/src/Api/UnauthorizedException.php b/plugins/woocommerce/src/Api/UnauthorizedException.php
deleted file mode 100644
index f75c27fb2f2..00000000000
--- a/plugins/woocommerce/src/Api/UnauthorizedException.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Thrown to deny access with a 401 Unauthorized status.
- *
- * Use when authentication is required but missing, or when an `authorize()`
- * method needs to deny access without distinguishing further. For credentials
- * that are present but rejected, prefer {@see InvalidTokenException}; for
- * "authenticated but not allowed", prefer {@see ForbiddenException}.
- *
- * Wire shape: `extensions.code = 'UNAUTHORIZED'`, HTTP status 401.
- */
-class UnauthorizedException extends ApiException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param array $extensions Additional error metadata to surface in the GraphQL `extensions` object.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message = 'Authentication required.',
- array $extensions = array(),
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, 'UNAUTHORIZED', $extensions, 401, $previous );
- }
-}
diff --git a/plugins/woocommerce/src/Api/Utils/Coupons/CouponMapper.php b/plugins/woocommerce/src/Api/Utils/Coupons/CouponMapper.php
deleted file mode 100644
index 898f8e7efb8..00000000000
--- a/plugins/woocommerce/src/Api/Utils/Coupons/CouponMapper.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Utils\Coupons;
-
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\Types\Coupons\Coupon;
-
-/**
- * Maps a WC_Coupon to the Coupon DTO.
- */
-class CouponMapper {
- /**
- * Map a WC_Coupon to the Coupon DTO.
- *
- * @param \WC_Coupon $wc_coupon The WooCommerce coupon object.
- * @return Coupon
- */
- public static function from_wc_coupon( \WC_Coupon $wc_coupon ): Coupon {
- $coupon = new Coupon();
-
- $raw_discount_type = (string) $wc_coupon->get_discount_type();
- $raw_status = (string) $wc_coupon->get_status();
-
- $coupon->id = $wc_coupon->get_id();
- $coupon->code = $wc_coupon->get_code();
- $coupon->description = $wc_coupon->get_description();
- $coupon->discount_type = DiscountType::tryFrom( $raw_discount_type ) ?? DiscountType::Other;
- $coupon->raw_discount_type = $raw_discount_type;
- $coupon->amount = (float) $wc_coupon->get_amount();
- $coupon->status = '' === $raw_status
- ? CouponStatus::Draft
- : ( CouponStatus::tryFrom( $raw_status ) ?? CouponStatus::Other );
- $coupon->raw_status = $raw_status;
- $coupon->date_created = $wc_coupon->get_date_created()?->format( \DateTimeInterface::ATOM );
- $coupon->date_modified = $wc_coupon->get_date_modified()?->format( \DateTimeInterface::ATOM );
- $coupon->date_expires = $wc_coupon->get_date_expires()?->format( \DateTimeInterface::ATOM );
- $coupon->usage_count = $wc_coupon->get_usage_count();
- $coupon->individual_use = $wc_coupon->get_individual_use();
- $coupon->product_ids = $wc_coupon->get_product_ids();
- $coupon->excluded_product_ids = $wc_coupon->get_excluded_product_ids();
- $coupon->usage_limit = $wc_coupon->get_usage_limit();
- $coupon->usage_limit_per_user = $wc_coupon->get_usage_limit_per_user();
- $coupon->limit_usage_to_x_items = $wc_coupon->get_limit_usage_to_x_items();
- $coupon->free_shipping = $wc_coupon->get_free_shipping();
- $coupon->product_categories = $wc_coupon->get_product_categories();
- $coupon->excluded_product_categories = $wc_coupon->get_excluded_product_categories();
- $coupon->exclude_sale_items = $wc_coupon->get_exclude_sale_items();
- $coupon->minimum_amount = (float) $wc_coupon->get_minimum_amount();
- $coupon->maximum_amount = (float) $wc_coupon->get_maximum_amount();
- $coupon->email_restrictions = $wc_coupon->get_email_restrictions();
- $coupon->used_by = $wc_coupon->get_used_by();
-
- return $coupon;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Utils/Products/ProductMapper.php b/plugins/woocommerce/src/Api/Utils/Products/ProductMapper.php
deleted file mode 100644
index 04c4bebc265..00000000000
--- a/plugins/woocommerce/src/Api/Utils/Products/ProductMapper.php
+++ /dev/null
@@ -1,564 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Utils\Products;
-
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\Enums\Products\ProductType;
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\Edge;
-use Automattic\WooCommerce\Api\Pagination\IdCursorFilter;
-use Automattic\WooCommerce\Api\Pagination\PageInfo;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Api\Types\Products\ExternalProduct;
-use Automattic\WooCommerce\Api\Types\Products\ProductAttribute;
-use Automattic\WooCommerce\Api\Types\Products\ProductDimensions;
-use Automattic\WooCommerce\Api\Types\Products\ProductImage;
-use Automattic\WooCommerce\Api\Types\Products\ProductReview;
-use Automattic\WooCommerce\Api\Types\Products\ProductVariation;
-use Automattic\WooCommerce\Api\Types\Products\SelectedAttribute;
-use Automattic\WooCommerce\Api\Types\Products\SimpleProduct;
-use Automattic\WooCommerce\Api\Types\Products\VariableProduct;
-
-/**
- * Maps a WC_Product to the appropriate product DTO.
- */
-class ProductMapper {
- /**
- * Map a WC_Product to the appropriate product DTO based on its type.
- *
- * @param \WC_Product $wc_product The WooCommerce product object.
- * @param ?array $query_info Unified query info tree from the GraphQL request.
- * @return object
- */
- public static function from_wc_product(
- \WC_Product $wc_product,
- ?array $query_info = null,
- ): object {
- $product = match ( $wc_product->get_type() ) {
- 'external' => self::build_external_product( $wc_product ),
- 'variable' => self::build_variable_product( $wc_product, $query_info ),
- 'variation' => self::build_product_variation( $wc_product ),
- default => new SimpleProduct(),
- };
-
- self::populate_common_fields( $product, $wc_product, $query_info );
-
- return $product;
- }
-
- /**
- * Build an ExternalProduct with type-specific fields.
- *
- * @param \WC_Product $wc_product The external product.
- * @return ExternalProduct
- */
- private static function build_external_product( \WC_Product $wc_product ): ExternalProduct {
- $product = new ExternalProduct();
-
- $url = $wc_product->get_product_url();
- $product->product_url = ! empty( $url ) ? $url : null;
- $text = $wc_product->get_button_text();
- $product->button_text = ! empty( $text ) ? $text : null;
-
- return $product;
- }
-
- /**
- * Build a VariableProduct with type-specific fields.
- *
- * @param \WC_Product $wc_product The variable product.
- * @param ?array $query_info Unified query info tree from the GraphQL request.
- * @return VariableProduct
- */
- private static function build_variable_product( \WC_Product $wc_product, ?array $query_info = null ): VariableProduct {
- $product = new VariableProduct();
-
- $child_ids = $wc_product->get_children();
- $total_count = count( $child_ids );
-
- // Extract the per-variation selection and pagination args from
- // $query_info up front. Narrowing $query_info keeps recursive
- // from_wc_product() calls from fetching subtrees the client didn't
- // request (e.g. reviews for every variation).
- $variations_info = $query_info['...VariableProduct']['variations']
- ?? $query_info['variations']
- ?? null;
- $variation_query_info = self::connection_node_info( $variations_info );
- $pagination_args = $variations_info['__args'] ?? array();
-
- // Slice the ID window *before* mapping: otherwise `variations(first: 1)`
- // on a product with N variations would prime+map all N just to slice
- // the result down afterwards. The resolver-level validation at
- // Connection::slice() is now bypassed (we're building a pre-sliced
- // connection), so call validate_args() explicitly to keep the 0..
- // MAX_PAGE_SIZE bounds enforced.
- PaginationParams::validate_args( $pagination_args );
- $page = self::slice_variation_ids( $child_ids, $pagination_args );
-
- // Prime post + meta caches for only the paged subset.
- if ( ! empty( $page['ids'] ) ) {
- _prime_post_caches( $page['ids'] );
- }
-
- $edges = array();
- $nodes = array();
- foreach ( $page['ids'] as $child_id ) {
- $child_product = wc_get_product( $child_id );
- if ( ! $child_product ) {
- continue;
- }
-
- $variation = self::from_wc_product( $child_product, $variation_query_info );
-
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $child_id );
- $edge->node = $variation;
-
- $edges[] = $edge;
- $nodes[] = $variation;
- }
-
- $page_info = new PageInfo();
- $page_info->has_next_page = $page['has_next_page'];
- $page_info->has_previous_page = $page['has_previous_page'];
- $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
- $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
-
- // total_count reflects the full variation set, not the paged one —
- // consistent with how the root list resolvers compute it.
- $product->variations = Connection::pre_sliced( $edges, $page_info, $total_count );
-
- return $product;
- }
-
- /**
- * Compute a Relay cursor page against a list of variation IDs.
- *
- * Mirrors the logic in {@see Connection::slice()} but operates on raw
- * IDs so the caller can page-down *before* calling `wc_get_product()`
- * + `from_wc_product()` on each child. Returns the paged IDs and the
- * corresponding `has_next_page` / `has_previous_page` flags in Relay
- * semantics.
- *
- * @param int[] $child_ids Full variation ID list, in menu_order.
- * @param array $args `{first?, last?, after?, before?}` raw GraphQL args.
- * @return array{ids: int[], has_next_page: bool, has_previous_page: bool}
- */
- private static function slice_variation_ids( array $child_ids, array $args ): array {
- $first = $args['first'] ?? null;
- $last = $args['last'] ?? null;
- $after = $args['after'] ?? null;
- $before = $args['before'] ?? null;
-
- // No pagination requested — return the full list as-is.
- if ( null === $first && null === $last && null === $after && null === $before ) {
- return array(
- 'ids' => array_values( $child_ids ),
- 'has_next_page' => false,
- 'has_previous_page' => false,
- );
- }
-
- // Narrow by `after`: drop IDs up to and including the cursor position.
- if ( null !== $after ) {
- $after_id = IdCursorFilter::decode_id_cursor( $after, 'after' );
- $idx = array_search( $after_id, $child_ids, true );
- $child_ids = false !== $idx ? array_slice( $child_ids, $idx + 1 ) : array();
- }
-
- // Narrow by `before`: drop IDs from the cursor position onward.
- if ( null !== $before ) {
- $before_id = IdCursorFilter::decode_id_cursor( $before, 'before' );
- $idx = array_search( $before_id, $child_ids, true );
- if ( false !== $idx ) {
- $child_ids = array_slice( $child_ids, 0, $idx );
- }
- }
-
- $total_after_cursors = count( $child_ids );
-
- // Apply first/last limits.
- if ( null !== $first && $first >= 0 ) {
- $child_ids = array_slice( $child_ids, 0, $first );
- }
- if ( null !== $last && $last >= 0 ) {
- $child_ids = array_slice( $child_ids, max( 0, count( $child_ids ) - $last ) );
- }
-
- // Relay semantics for the forward / backward branches match what
- // ListProducts / ListCoupons use at the root level.
- return array(
- 'ids' => array_values( $child_ids ),
- 'has_next_page' =>
- null !== $first ? count( $child_ids ) < $total_after_cursors : ( null !== $before ),
- 'has_previous_page' =>
- null !== $last ? count( $child_ids ) < $total_after_cursors : ( null !== $after ),
- );
- }
-
- /**
- * Build a ProductVariation with type-specific fields.
- *
- * @param \WC_Product $wc_product The variation product.
- * @return ProductVariation
- */
- private static function build_product_variation( \WC_Product $wc_product ): ProductVariation {
- $product = new ProductVariation();
- $product->parent_id = $wc_product->get_parent_id();
-
- $selected_attributes = array();
- foreach ( $wc_product->get_attributes() as $taxonomy => $value ) {
- $attr = new SelectedAttribute();
- $attr->name = $taxonomy;
-
- // For taxonomy attributes, resolve the slug to a human-readable term name.
- if ( taxonomy_exists( $taxonomy ) && ! empty( $value ) ) {
- $term = get_term_by( 'slug', $value, $taxonomy );
- if ( $term && ! is_wp_error( $term ) ) {
- $attr->value = $term->name;
- } else {
- $attr->value = $value;
- }
- } else {
- $attr->value = $value;
- }
-
- $selected_attributes[] = $attr;
- }
- $product->selected_attributes = $selected_attributes;
-
- return $product;
- }
-
- /**
- * Populate the common fields shared by all product types.
- *
- * @param object $product The product DTO to populate.
- * @param \WC_Product $wc_product The WooCommerce product object.
- * @param ?array $query_info Unified query info tree from the GraphQL request.
- */
- private static function populate_common_fields(
- object $product,
- \WC_Product $wc_product,
- ?array $query_info,
- ): void {
- $raw_status = (string) $wc_product->get_status();
- $raw_product_type = (string) $wc_product->get_type();
-
- $product->id = $wc_product->get_id();
- $product->name = $wc_product->get_name();
- $product->slug = $wc_product->get_slug();
- $sku = $wc_product->get_sku();
- $product->sku = '' !== $sku ? $sku : null;
- $product->description = $wc_product->get_description();
- $product->short_description = $wc_product->get_short_description();
- $product->status = ProductStatus::tryFrom( $raw_status ) ?? ProductStatus::Other;
- $product->raw_status = $raw_status;
- $product->product_type = ProductType::tryFrom( $raw_product_type ) ?? ProductType::Other;
- $product->raw_product_type = $raw_product_type;
-
- // Price fields support a "formatted" argument for currency display.
- // An empty stored value means "not set" and is surfaced as null —
- // without this, wc_price( (float) '' ) would render as "$0.00" and
- // be indistinguishable from a genuinely-zero price.
- $format_regular = $query_info['regular_price']['__args']['formatted'] ?? true;
- $raw_regular = $wc_product->get_regular_price();
- if ( '' === $raw_regular ) {
- $product->regular_price = null;
- } else {
- $product->regular_price = $format_regular
- ? wc_price( (float) $raw_regular )
- : $raw_regular;
- }
-
- $format_sale = $query_info['sale_price']['__args']['formatted'] ?? true;
- $raw_sale = $wc_product->get_sale_price();
- if ( '' === $raw_sale ) {
- $product->sale_price = null;
- } else {
- $product->sale_price = $format_sale
- ? wc_price( (float) $raw_sale )
- : $raw_sale;
- }
-
- $raw_stock_status = (string) $wc_product->get_stock_status();
- $product->stock_status = self::map_stock_status( $raw_stock_status );
- $product->raw_stock_status = $raw_stock_status;
- $product->stock_quantity = $wc_product->get_stock_quantity();
-
- // Nested output type: dimensions.
- $product->dimensions = self::build_dimensions( $wc_product );
-
- // Array of objects: images.
- $product->images = self::build_images( $wc_product );
-
- // Array of objects: attributes.
- $product->attributes = self::build_attributes( $wc_product );
-
- // Sub-collection connection: reviews.
- // Only populate if explicitly requested (optimization via $query_info).
- if ( null === $query_info || array_key_exists( 'reviews', $query_info ) ) {
- $product->reviews = self::build_reviews( $wc_product->get_id() );
- } else {
- $product->reviews = self::empty_connection();
- }
-
- $product->date_created = $wc_product->get_date_created()?->format( \DateTimeInterface::ATOM );
- $product->date_modified = $wc_product->get_date_modified()?->format( \DateTimeInterface::ATOM );
-
- // Ignored field — set to null; it won't appear in the schema.
- $product->internal_notes = null;
- }
-
- /**
- * Map WooCommerce stock status string to the int-backed StockStatus enum.
- *
- * @param string $wc_status The WC stock status string.
- * @return StockStatus
- */
- private static function map_stock_status( string $wc_status ): StockStatus {
- return match ( $wc_status ) {
- 'instock' => StockStatus::InStock,
- 'outofstock' => StockStatus::OutOfStock,
- 'onbackorder' => StockStatus::OnBackorder,
- default => StockStatus::Other,
- };
- }
-
- /**
- * Build product dimensions from a WC_Product.
- *
- * @param \WC_Product $wc_product The product.
- * @return ?ProductDimensions
- */
- private static function build_dimensions( \WC_Product $wc_product ): ?ProductDimensions {
- $length = $wc_product->get_length();
- $width = $wc_product->get_width();
- $height = $wc_product->get_height();
- $weight = $wc_product->get_weight();
-
- if ( '' === $length && '' === $width && '' === $height && '' === $weight ) {
- return null;
- }
-
- $dims = new ProductDimensions();
- $dims->length = '' !== $length ? (float) $length : null;
- $dims->width = '' !== $width ? (float) $width : null;
- $dims->height = '' !== $height ? (float) $height : null;
- $dims->weight = '' !== $weight ? (float) $weight : null;
-
- return $dims;
- }
-
- /**
- * Build product images from a WC_Product.
- *
- * @param \WC_Product $wc_product The product.
- * @return ProductImage[]
- */
- private static function build_images( \WC_Product $wc_product ): array {
- $images = array();
- $position = 0;
-
- // Include the featured image first.
- $featured_id = $wc_product->get_image_id();
- if ( $featured_id ) {
- $image = self::build_image( (int) $featured_id, $position );
- if ( null !== $image ) {
- $images[] = $image;
- ++$position;
- }
- }
-
- // Then gallery images.
- foreach ( $wc_product->get_gallery_image_ids() as $image_id ) {
- $image = self::build_image( (int) $image_id, $position );
- if ( null !== $image ) {
- $images[] = $image;
- ++$position;
- }
- }
-
- return $images;
- }
-
- /**
- * Build product attributes from a WC_Product.
- *
- * For variations, attributes are simple key→value pairs (handled by selected_attributes),
- * so this returns an empty array. For other product types, it returns full attribute definitions.
- *
- * @param \WC_Product $wc_product The product.
- * @return ProductAttribute[]
- */
- private static function build_attributes( \WC_Product $wc_product ): array {
- // Variations store attributes as simple string values, not WC_Product_Attribute objects.
- if ( 'variation' === $wc_product->get_type() ) {
- return array();
- }
-
- $attributes = array();
- foreach ( $wc_product->get_attributes() as $wc_attr ) {
- if ( ! $wc_attr instanceof \WC_Product_Attribute ) {
- continue;
- }
-
- $attr = new ProductAttribute();
- $attr->slug = $wc_attr->get_name();
-
- if ( $wc_attr->is_taxonomy() ) {
- $attr->name = wc_attribute_label( $wc_attr->get_name() );
- $attr->options = array_map(
- function ( $term ) {
- return $term->name;
- },
- $wc_attr->get_terms() ? $wc_attr->get_terms() : array()
- );
- } else {
- $attr->name = $wc_attr->get_name();
- $attr->options = $wc_attr->get_options();
- }
-
- $attr->position = $wc_attr->get_position();
- $attr->visible = $wc_attr->get_visible();
- $attr->variation = $wc_attr->get_variation();
- $attr->is_taxonomy = $wc_attr->is_taxonomy();
-
- $attributes[] = $attr;
- }//end foreach
-
- return $attributes;
- }
-
- /**
- * Build a single ProductImage from an attachment ID.
- *
- * @param int $attachment_id The WordPress attachment ID.
- * @param int $position The display position.
- * @return ?ProductImage
- */
- private static function build_image( int $attachment_id, int $position ): ?ProductImage {
- $url = wp_get_attachment_url( $attachment_id );
- if ( ! $url ) {
- return null;
- }
-
- $image = new ProductImage();
- $image->id = $attachment_id;
- $image->url = $url;
- $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
- $image->alt = ! empty( $alt ) ? $alt : '';
- $image->position = $position;
-
- return $image;
- }
-
- /**
- * Build a reviews connection for a product.
- *
- * @param int $product_id The product ID.
- * @return Connection
- */
- private static function build_reviews( int $product_id ): Connection {
- $base_args = array(
- 'post_id' => $product_id,
- 'type' => 'review',
- 'status' => 'approve',
- );
-
- // Separate count query: otherwise `total_count` would be the page
- // size (capped at 10) instead of the real review total.
- $total_count = (int) get_comments( $base_args + array( 'count' => true ) );
-
- $comments = get_comments(
- $base_args + array(
- 'orderby' => 'comment_date',
- 'order' => 'DESC',
- 'number' => 10,
- )
- );
-
- $edges = array();
- $nodes = array();
-
- foreach ( $comments as $comment ) {
- $review = new ProductReview();
- $review->id = (int) $comment->comment_ID;
- $review->product_id = $product_id;
- $review->reviewer = $comment->comment_author;
- $review->review = $comment->comment_content;
- $review->rating = (int) get_comment_meta( $comment->comment_ID, 'rating', true );
- $review->date_created = $comment->comment_date_gmt
- ? ( new \DateTimeImmutable( $comment->comment_date_gmt, new \DateTimeZone( 'UTC' ) ) )->format( \DateTimeInterface::ATOM )
- : null;
-
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $review->id );
- $edge->node = $review;
-
- $edges[] = $edge;
- $nodes[] = $review;
- }
-
- $page_info = new PageInfo();
- $page_info->has_next_page = $total_count > count( $comments );
- $page_info->has_previous_page = false;
- $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null;
- $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null;
-
- $connection = new Connection();
- $connection->edges = $edges;
- $connection->nodes = $nodes;
- $connection->page_info = $page_info;
- $connection->total_count = $total_count;
-
- return $connection;
- }
-
- /**
- * Extract the per-node selection from a connection's query_info entry.
- *
- * Connections can be queried via `nodes { ... }` (the plain form) or
- * `edges { node { ... } }` (Relay form); clients may use either or both.
- * The per-node selection is what gets forwarded to the recursive
- * mapper call so each node is built with the right sub-fields.
- *
- * @param ?array $connection_info The query_info entry for the connection (e.g. `$query_info['variations']`).
- * @return ?array The merged per-node selection, or null when the caller didn't request any node fields.
- */
- public static function connection_node_info( ?array $connection_info ): ?array {
- if ( null === $connection_info ) {
- return null;
- }
- $nodes = is_array( $connection_info['nodes'] ?? null ) ? $connection_info['nodes'] : array();
- $edge = is_array( $connection_info['edges']['node'] ?? null ) ? $connection_info['edges']['node'] : array();
- if ( empty( $nodes ) && empty( $edge ) ) {
- return null;
- }
- return array_merge( $edge, $nodes );
- }
-
- /**
- * Return an empty connection (for skipped sub-collections).
- *
- * @return Connection
- */
- private static function empty_connection(): Connection {
- $page_info = new PageInfo();
- $page_info->has_next_page = false;
- $page_info->has_previous_page = false;
- $page_info->start_cursor = null;
- $page_info->end_cursor = null;
-
- $connection = new Connection();
- $connection->edges = array();
- $connection->nodes = array();
- $connection->page_info = $page_info;
- $connection->total_count = 0;
-
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/src/Api/Utils/Products/ProductRepository.php b/plugins/woocommerce/src/Api/Utils/Products/ProductRepository.php
deleted file mode 100644
index 99e8a641ff9..00000000000
--- a/plugins/woocommerce/src/Api/Utils/Products/ProductRepository.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Utils\Products;
-
-/**
- * Repository for product persistence operations.
- *
- * Designed to be injected via the DI container into commands
- * that need to load or save products.
- */
-class ProductRepository {
- /**
- * Find a product by ID.
- *
- * @param int $id The product ID.
- * @return ?\WC_Product The product, or null if not found.
- */
- public function find( int $id ): ?\WC_Product {
- $product = wc_get_product( $id );
- return $product instanceof \WC_Product ? $product : null;
- }
-
- /**
- * Save a product.
- *
- * @param \WC_Product $product The product to save.
- */
- public function save( \WC_Product $product ): void {
- $product->save();
- }
-}
diff --git a/plugins/woocommerce/src/Api/Utils/SchemaHandle.php b/plugins/woocommerce/src/Api/Utils/SchemaHandle.php
deleted file mode 100644
index 9bbceb76629..00000000000
--- a/plugins/woocommerce/src/Api/Utils/SchemaHandle.php
+++ /dev/null
@@ -1,290 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api\Utils;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-
-/**
- * Opaque handle to a dual-API GraphQL schema, exposing the runtime inspection
- * operations the dual-API surface supports.
- *
- * The handle wraps the live engine schema but does not expose it. Clients
- * therefore depend only on the methods this class declares — never on the
- * underlying engine type — which keeps a future engine swap as a non-public
- * API change.
- *
- * Construction is reserved for the dual-API infrastructure. Obtain a handle
- * via your dual-API `GraphQLController`'s `get_schema()` method:
- *
- * $schema = wc_get_container()
- * ->get( \Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLController::class )
- * ->get_schema();
- *
- * WooCommerce plugins implementing their own dual API reach a handle through
- * their own concrete autogenerated controller the same way.
- *
- * The current public surface is the discovery channel: {@see self::get_all_metadata()}
- * returns every row in the schema that carries either `#[Metadata]`-derived
- * entries or authorization attributes, and {@see self::find_metadata()} applies
- * filter-narrows semantics (`name`, `type`, `field`, `attribute`) over the same
- * set. Authorization descriptors are exposed as a parallel `authorization`
- * slice on each row, alongside the existing `entries`.
- *
- * @phpstan-type MetadataRow array{
- * type: string,
- * field: ?string,
- * argument: ?string,
- * enumValue: ?string,
- * entries: array<string, bool|int|float|string|null>,
- * authorization: list<array{attribute: string, args: list<mixed>}>
- * }
- */
-final class SchemaHandle {
-
- /**
- * The wrapped engine schema. Typed as `object` (rather than the engine's
- * `Schema` class) so the class signature carries no engine-specific
- * symbol; the inspection methods cast to engine APIs internally.
- *
- * @var object
- */
- private object $engine_schema;
-
- /**
- * Wrap an engine schema in a handle.
- *
- * @internal Reserved for dual-API infrastructure (the controller's `get_schema()` accessor and similarly placed code). Plugins obtain a handle through their own controller, not by instantiating directly.
- *
- * @param object $engine_schema Engine-specific schema instance the handle wraps.
- */
- public function __construct( object $engine_schema ) {
- $this->engine_schema = $engine_schema;
- }
-
- /**
- * Return every metadata row in the schema (introspection types excluded).
- *
- * Each row describes one *target* (a type, a field, an argument, or an
- * enum value) and carries the name=>value entries declared on it. The
- * same row shape is used for every target kind; the three nullable
- * position fields (`field`, `argument`, `enumValue`) discriminate.
- *
- * @return list<array{type: string, field: ?string, argument: ?string, enumValue: ?string, entries: array<string, bool|int|float|string|null>, authorization: list<array{attribute: string, args: list<mixed>}>}>
- */
- public function get_all_metadata(): array {
- $rows = array();
- $schema = $this->engine_schema;
-
- foreach ( $schema->getTypeMap() as $type_name => $type ) {
- if ( self::is_introspection_name( $type_name ) ) {
- continue;
- }
-
- $type_metadata = self::read_type_metadata( $type );
- $type_authorization = self::read_type_authorization( $type );
- if ( ! empty( $type_metadata ) || ! empty( $type_authorization ) ) {
- $rows[] = self::make_row( $type_name, null, null, null, $type_metadata, $type_authorization );
- }
-
- if ( $type instanceof HasFieldsType ) {
- foreach ( $type->getFields() as $field_name => $field ) {
- $field_metadata = self::read_element_metadata( $field );
- $field_authorization = self::read_element_authorization( $field );
- if ( ! empty( $field_metadata ) || ! empty( $field_authorization ) ) {
- $rows[] = self::make_row( $type_name, $field_name, null, null, $field_metadata, $field_authorization );
- }
-
- foreach ( $field->args as $arg ) {
- $arg_metadata = self::read_element_metadata( $arg );
- if ( ! empty( $arg_metadata ) ) {
- $rows[] = self::make_row( $type_name, $field_name, $arg->name, null, $arg_metadata, array() );
- }
- }
- }
- continue;
- }
-
- if ( $type instanceof InputObjectType ) {
- foreach ( $type->getFields() as $field_name => $field ) {
- $field_metadata = self::read_element_metadata( $field );
- $field_authorization = self::read_element_authorization( $field );
- if ( ! empty( $field_metadata ) || ! empty( $field_authorization ) ) {
- $rows[] = self::make_row( $type_name, $field_name, null, null, $field_metadata, $field_authorization );
- }
- }
- continue;
- }
-
- if ( $type instanceof EnumType ) {
- foreach ( $type->getValues() as $value ) {
- $value_metadata = self::read_element_metadata( $value );
- if ( ! empty( $value_metadata ) ) {
- $rows[] = self::make_row( $type_name, null, null, $value->name, $value_metadata, array() );
- }
- }
- }
- }
-
- return $rows;
- }
-
- /**
- * Filter-narrows view over {@see self::get_all_metadata()}.
- *
- * Each filter argument independently restricts the result set; supplying
- * multiple composes as AND. When `$name` is supplied, the surviving rows
- * have their `entries` trimmed to the single matching entry; so a caller
- * asking "which elements are marked X" gets focused rows back, not the
- * full multi-entry shape.
- *
- * @param ?string $name Optional metadata name to match. When set, only rows containing this entry survive and their `entries` are trimmed to it.
- * @param ?string $type Optional GraphQL type name to match.
- * @param ?string $field Optional GraphQL field name to match.
- * @param ?string $attribute Optional authorization-attribute short name to match. When set, only rows carrying this attribute survive and their `authorization` is trimmed to the matching descriptors.
- *
- * @return list<array{type: string, field: ?string, argument: ?string, enumValue: ?string, entries: array<string, bool|int|float|string|null>, authorization: list<array{attribute: string, args: list<mixed>}>}>
- */
- public function find_metadata( ?string $name = null, ?string $type = null, ?string $field = null, ?string $attribute = null ): array {
- $rows = $this->get_all_metadata();
-
- $result = array();
- foreach ( $rows as $row ) {
- if ( null !== $type && $row['type'] !== $type ) {
- continue;
- }
- if ( null !== $field && $row['field'] !== $field ) {
- continue;
- }
- if ( null !== $name ) {
- if ( ! array_key_exists( $name, $row['entries'] ) ) {
- continue;
- }
- $row['entries'] = array( $name => $row['entries'][ $name ] );
- }
- if ( null !== $attribute ) {
- $matching = array_values(
- array_filter(
- $row['authorization'],
- static fn( array $descriptor ): bool => ( $descriptor['attribute'] ?? null ) === $attribute,
- )
- );
- if ( empty( $matching ) ) {
- continue;
- }
- $row['authorization'] = $matching;
- }
- $result[] = $row;
- }
-
- return $result;
- }
-
- /**
- * Read type-level metadata from a wrapped engine type.
- *
- * The wrapper subclasses in `Internal/Api/Schema/` expose `get_metadata()`;
- * non-wrapper types (e.g. the built-in scalars, the introspection types we
- * already filtered out) don't carry metadata and contribute an empty array.
- *
- * @param Type $type The GraphQL type to inspect.
- * @return array<string, bool|int|float|string|null>
- */
- private static function read_type_metadata( Type $type ): array {
- if ( method_exists( $type, 'get_metadata' ) ) {
- $metadata = $type->get_metadata();
- return is_array( $metadata ) ? $metadata : array();
- }
- return array();
- }
-
- /**
- * Read field-/arg-/enum-value-level metadata from the original config array.
- *
- * FieldDefinition, Argument, InputObjectField and EnumValueDefinition all
- * preserve their construction config in a public `$config` property, so
- * the `metadata` key emitted by ApiBuilder is reachable here without any
- * wrapper-side plumbing.
- *
- * @param object $element FieldDefinition | Argument | InputObjectField | EnumValueDefinition.
- * @return array<string, bool|int|float|string|null>
- */
- private static function read_element_metadata( object $element ): array {
- if ( ! property_exists( $element, 'config' ) ) {
- return array();
- }
- $metadata = $element->config['metadata'] ?? array();
- return is_array( $metadata ) ? $metadata : array();
- }
-
- /**
- * Read authorization descriptors attached to a wrapped engine type.
- *
- * The wrapper subclasses preserve the original config in `$type->config`;
- * authorization descriptors emitted by ApiBuilder live under the
- * `authorization` key as a list of `{attribute, args}` records.
- *
- * @param Type $type The GraphQL type to inspect.
- * @return list<array{attribute: string, args: list<mixed>}>
- */
- private static function read_type_authorization( Type $type ): array {
- if ( ! property_exists( $type, 'config' ) ) {
- return array();
- }
- $authorization = $type->config['authorization'] ?? array();
- return is_array( $authorization ) ? $authorization : array();
- }
-
- /**
- * Read authorization descriptors from a field-/arg-/enum-value-level config.
- *
- * Mirrors {@see self::read_element_metadata()} but pulls the
- * `authorization` key. Returns an empty list when the element carries
- * no authorization descriptors.
- *
- * @param object $element FieldDefinition | Argument | InputObjectField | EnumValueDefinition.
- * @return list<array{attribute: string, args: list<mixed>}>
- */
- private static function read_element_authorization( object $element ): array {
- if ( ! property_exists( $element, 'config' ) ) {
- return array();
- }
- $authorization = $element->config['authorization'] ?? array();
- return is_array( $authorization ) ? $authorization : array();
- }
-
- /**
- * Build a metadata row in the standard shape.
- *
- * @param string $type GraphQL type name.
- * @param ?string $field Field name when the row describes a field; null otherwise.
- * @param ?string $argument Argument name when the row describes a field argument; null otherwise.
- * @param ?string $enum_value Enum value name when the row describes an enum value; null otherwise.
- * @param array<string, bool|int|float|string|null> $entries Name=>value entries to attach to the row.
- * @param list<array{attribute: string, args: list<mixed>}> $authorization Authorization descriptors attached to the row, or an empty list.
- * @return array{type: string, field: ?string, argument: ?string, enumValue: ?string, entries: array<string, bool|int|float|string|null>, authorization: list<array{attribute: string, args: list<mixed>}>}
- */
- private static function make_row( string $type, ?string $field, ?string $argument, ?string $enum_value, array $entries, array $authorization ): array {
- return array(
- 'type' => $type,
- 'field' => $field,
- 'argument' => $argument,
- 'enumValue' => $enum_value,
- 'entries' => $entries,
- 'authorization' => $authorization,
- );
- }
-
- /**
- * Whether a type name belongs to GraphQL's introspection system (and so should be skipped).
- *
- * @param string $name Type name.
- */
- private static function is_introspection_name( string $name ): bool {
- return str_starts_with( $name, '__' );
- }
-}
diff --git a/plugins/woocommerce/src/Api/ValidationException.php b/plugins/woocommerce/src/Api/ValidationException.php
deleted file mode 100644
index 1bebbf5ee67..00000000000
--- a/plugins/woocommerce/src/Api/ValidationException.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Api;
-
-/**
- * Thrown to signal that input is well-formed but failed business-rule
- * validation, e.g. a required field was empty, two fields contradict each
- * other, a value violates a domain constraint.
- *
- * For purely structural input errors (wrong type, malformed shape) prefer
- * letting the framework's `\InvalidArgumentException` handling do the work:
- * `Utils::translate_exceptions()` already maps it to `INVALID_ARGUMENT` (400).
- *
- * Wire shape: `extensions.code = 'VALIDATION_ERROR'`, HTTP status 422.
- */
-class ValidationException extends ApiException {
- /**
- * Constructor.
- *
- * @param string $message The error message.
- * @param array $extensions Additional error metadata to surface in the GraphQL `extensions` object.
- * @param ?\Throwable $previous The previous throwable for chaining.
- */
- public function __construct(
- string $message = 'Validation failed.',
- array $extensions = array(),
- ?\Throwable $previous = null,
- ) {
- parent::__construct( $message, 'VALIDATION_ERROR', $extensions, 422, $previous );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLController.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLController.php
index 5661203e13b..831805ca71b 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLController.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLController.php
@@ -1,32 +1,34 @@
<?php
-declare(strict_types=1);
+/**
+ * GraphQL controller of the dual API proof of concept (compatibility stub).
+ */
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
+declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-
-class GraphQLController extends \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase {
- protected function build_schema(): Schema {
- return new Schema(
- array(
- 'query' => RootQueryType::get(),
- 'mutation' => RootMutationType::get(),
- 'types' => TypeRegistry::get_interface_implementors(),
- )
- );
- }
-
- protected function get_class_resolver_fqcn(): ?string {
- return \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::class;
- }
+defined( 'ABSPATH' ) || exit;
- protected function get_principal_resolver_fqcn(): ?string {
- return \Automattic\WooCommerce\Api\Infrastructure\PrincipalResolver::class;
- }
+/**
+ * Compatibility stub for the generated GraphQL controller of the dual API proof of concept.
+ *
+ * The dual (code + GraphQL) API engine and its proof-of-concept core API were removed from
+ * WooCommerce core in 11.2 (the engine now lives in the WooCommerce Dual API plugin). This
+ * empty stub stays so that, during an in-place update from 10.9-11.1, the still-resident
+ * `Main::handle_rest_api_init_for_core()` of the old version can instantiate the class it
+ * names instead of fataling on the deleted file when the REST server boots later in the same
+ * request (see the wc-admin settings preload on `admin_print_footer_scripts`). It registers
+ * nothing.
+ *
+ * The old code only reaches this class when the hidden `dual_code_graphql_api` feature flag
+ * is on, which experimenters enabled through `wp option update`.
+ *
+ * @deprecated 11.2.0
+ */
+class GraphQLController {
- protected function principal_resolver_takes_request(): bool {
- return false;
- }
+ /**
+ * Register the REST route. Intentionally a no-op.
+ */
+ public function register(): void {}
}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php
deleted file mode 100644
index b9f424535ef..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Coupons\CreateCoupon as CreateCouponCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\CreateCoupon as CreateCouponInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateCoupon {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( CouponType::get() ),
- 'description' => __( 'Create a new coupon.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( CreateCouponInput::get() ),
- 'description' => __( 'Data for the new coupon.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( CreateCouponCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_create_coupon_input( $args['input'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal );
- }
-
- private static function convert_create_coupon_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Coupons\CreateCouponInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Coupons\CreateCouponInput();
-
- if ( array_key_exists( 'code', $data ) ) {
- $input->mark_provided( 'code' );
- $input->code = $data['code'];
- }
- if ( array_key_exists( 'description', $data ) ) {
- $input->mark_provided( 'description' );
- $input->description = $data['description'];
- }
- if ( array_key_exists( 'discount_type', $data ) ) {
- $input->mark_provided( 'discount_type' );
- $input->discount_type = $data['discount_type'];
- }
- if ( array_key_exists( 'amount', $data ) ) {
- $input->mark_provided( 'amount' );
- $input->amount = $data['amount'];
- }
- if ( array_key_exists( 'status', $data ) ) {
- $input->mark_provided( 'status' );
- $input->status = $data['status'];
- }
- if ( array_key_exists( 'date_expires', $data ) ) {
- $input->mark_provided( 'date_expires' );
- $input->date_expires = $data['date_expires'];
- }
- if ( array_key_exists( 'individual_use', $data ) ) {
- $input->mark_provided( 'individual_use' );
- $input->individual_use = $data['individual_use'];
- }
- if ( array_key_exists( 'product_ids', $data ) ) {
- $input->mark_provided( 'product_ids' );
- $input->product_ids = $data['product_ids'];
- }
- if ( array_key_exists( 'excluded_product_ids', $data ) ) {
- $input->mark_provided( 'excluded_product_ids' );
- $input->excluded_product_ids = $data['excluded_product_ids'];
- }
- if ( array_key_exists( 'usage_limit', $data ) ) {
- $input->mark_provided( 'usage_limit' );
- $input->usage_limit = $data['usage_limit'];
- }
- if ( array_key_exists( 'usage_limit_per_user', $data ) ) {
- $input->mark_provided( 'usage_limit_per_user' );
- $input->usage_limit_per_user = $data['usage_limit_per_user'];
- }
- if ( array_key_exists( 'limit_usage_to_x_items', $data ) ) {
- $input->mark_provided( 'limit_usage_to_x_items' );
- $input->limit_usage_to_x_items = $data['limit_usage_to_x_items'];
- }
- if ( array_key_exists( 'free_shipping', $data ) ) {
- $input->mark_provided( 'free_shipping' );
- $input->free_shipping = $data['free_shipping'];
- }
- if ( array_key_exists( 'product_categories', $data ) ) {
- $input->mark_provided( 'product_categories' );
- $input->product_categories = $data['product_categories'];
- }
- if ( array_key_exists( 'excluded_product_categories', $data ) ) {
- $input->mark_provided( 'excluded_product_categories' );
- $input->excluded_product_categories = $data['excluded_product_categories'];
- }
- if ( array_key_exists( 'exclude_sale_items', $data ) ) {
- $input->mark_provided( 'exclude_sale_items' );
- $input->exclude_sale_items = $data['exclude_sale_items'];
- }
- if ( array_key_exists( 'minimum_amount', $data ) ) {
- $input->mark_provided( 'minimum_amount' );
- $input->minimum_amount = $data['minimum_amount'];
- }
- if ( array_key_exists( 'maximum_amount', $data ) ) {
- $input->mark_provided( 'maximum_amount' );
- $input->maximum_amount = $data['maximum_amount'];
- }
- if ( array_key_exists( 'email_restrictions', $data ) ) {
- $input->mark_provided( 'email_restrictions' );
- $input->email_restrictions = $data['email_restrictions'];
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php
deleted file mode 100644
index 386100e6513..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php
+++ /dev/null
@@ -1,156 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Products\CreateProduct as CreateProductCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\CreateProduct as CreateProductInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateProduct {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( ProductInterface::get() ),
- 'description' => __( 'Create a new product.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'edit_products',
- ),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( CreateProductInput::get() ),
- 'description' => __( 'Data for the new product.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( CreateProductCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_create_product_input( $args['input'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'edit_products' ) )->authorize( $principal );
- }
-
- private static function convert_dimensions_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput();
-
- if ( array_key_exists( 'length', $data ) ) {
- $input->mark_provided( 'length' );
- $input->length = $data['length'];
- }
- if ( array_key_exists( 'width', $data ) ) {
- $input->mark_provided( 'width' );
- $input->width = $data['width'];
- }
- if ( array_key_exists( 'height', $data ) ) {
- $input->mark_provided( 'height' );
- $input->height = $data['height'];
- }
- if ( array_key_exists( 'weight', $data ) ) {
- $input->mark_provided( 'weight' );
- $input->weight = $data['weight'];
- }
-
- return $input;
- }
-
- private static function convert_create_product_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Products\CreateProductInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Products\CreateProductInput();
-
- if ( array_key_exists( 'name', $data ) ) {
- $input->mark_provided( 'name' );
- $input->name = $data['name'];
- }
- if ( array_key_exists( 'slug', $data ) ) {
- $input->mark_provided( 'slug' );
- $input->slug = $data['slug'];
- }
- if ( array_key_exists( 'sku', $data ) ) {
- $input->mark_provided( 'sku' );
- $input->sku = $data['sku'];
- }
- if ( array_key_exists( 'description', $data ) ) {
- $input->mark_provided( 'description' );
- $input->description = $data['description'];
- }
- if ( array_key_exists( 'short_description', $data ) ) {
- $input->mark_provided( 'short_description' );
- $input->short_description = $data['short_description'];
- }
- if ( array_key_exists( 'status', $data ) ) {
- $input->mark_provided( 'status' );
- $input->status = $data['status'];
- }
- if ( array_key_exists( 'product_type', $data ) ) {
- $input->mark_provided( 'product_type' );
- $input->product_type = $data['product_type'];
- }
- if ( array_key_exists( 'regular_price', $data ) ) {
- $input->mark_provided( 'regular_price' );
- $input->regular_price = $data['regular_price'];
- }
- if ( array_key_exists( 'sale_price', $data ) ) {
- $input->mark_provided( 'sale_price' );
- $input->sale_price = $data['sale_price'];
- }
- if ( array_key_exists( 'manage_stock', $data ) ) {
- $input->mark_provided( 'manage_stock' );
- $input->manage_stock = $data['manage_stock'];
- }
- if ( array_key_exists( 'stock_quantity', $data ) ) {
- $input->mark_provided( 'stock_quantity' );
- $input->stock_quantity = $data['stock_quantity'];
- }
- if ( array_key_exists( 'dimensions', $data ) ) {
- $input->mark_provided( 'dimensions' );
- $input->dimensions = null !== $data['dimensions'] ? self::convert_dimensions_input( $data['dimensions'] ) : null;
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php
deleted file mode 100644
index 95c9ca7f98f..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Coupons\DeleteCoupon as DeleteCouponCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\DeleteCouponResult as DeleteCouponResultType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class DeleteCoupon {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( DeleteCouponResultType::get() ),
- 'description' => __( 'Delete a coupon.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the coupon to delete.', 'woocommerce' ),
- ),
- 'force' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether to permanently delete the coupon (bypass trash).', 'woocommerce' ),
- 'defaultValue' => false,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( DeleteCouponCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
- if ( array_key_exists( 'force', $args ) ) {
- $execute_args['force'] = $args['force'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php
deleted file mode 100644
index df9fa4d9b66..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Products\DeleteProduct as DeleteProductCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class DeleteProduct {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'DeleteProductResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::boolean() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Delete a product.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the product to delete.', 'woocommerce' ),
- ),
- 'force' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether to permanently delete the product (bypass trash).', 'woocommerce' ),
- 'defaultValue' => false,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( DeleteProductCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
- if ( array_key_exists( 'force', $args ) ) {
- $execute_args['force'] = $args['force'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php
deleted file mode 100644
index c7062f39e72..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php
+++ /dev/null
@@ -1,165 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Coupons\UpdateCoupon as UpdateCouponCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\UpdateCoupon as UpdateCouponInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class UpdateCoupon {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( CouponType::get() ),
- 'description' => __( 'Update an existing coupon.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( UpdateCouponInput::get() ),
- 'description' => __( 'The fields to update.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( UpdateCouponCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_update_coupon_input( $args['input'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal );
- }
-
- private static function convert_update_coupon_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Coupons\UpdateCouponInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Coupons\UpdateCouponInput();
-
- if ( array_key_exists( 'id', $data ) ) {
- $input->mark_provided( 'id' );
- $input->id = $data['id'];
- }
- if ( array_key_exists( 'code', $data ) ) {
- $input->mark_provided( 'code' );
- $input->code = $data['code'];
- }
- if ( array_key_exists( 'description', $data ) ) {
- $input->mark_provided( 'description' );
- $input->description = $data['description'];
- }
- if ( array_key_exists( 'discount_type', $data ) ) {
- $input->mark_provided( 'discount_type' );
- $input->discount_type = $data['discount_type'];
- }
- if ( array_key_exists( 'amount', $data ) ) {
- $input->mark_provided( 'amount' );
- $input->amount = $data['amount'];
- }
- if ( array_key_exists( 'status', $data ) ) {
- $input->mark_provided( 'status' );
- $input->status = $data['status'];
- }
- if ( array_key_exists( 'date_expires', $data ) ) {
- $input->mark_provided( 'date_expires' );
- $input->date_expires = $data['date_expires'];
- }
- if ( array_key_exists( 'individual_use', $data ) ) {
- $input->mark_provided( 'individual_use' );
- $input->individual_use = $data['individual_use'];
- }
- if ( array_key_exists( 'product_ids', $data ) ) {
- $input->mark_provided( 'product_ids' );
- $input->product_ids = $data['product_ids'];
- }
- if ( array_key_exists( 'excluded_product_ids', $data ) ) {
- $input->mark_provided( 'excluded_product_ids' );
- $input->excluded_product_ids = $data['excluded_product_ids'];
- }
- if ( array_key_exists( 'usage_limit', $data ) ) {
- $input->mark_provided( 'usage_limit' );
- $input->usage_limit = $data['usage_limit'];
- }
- if ( array_key_exists( 'usage_limit_per_user', $data ) ) {
- $input->mark_provided( 'usage_limit_per_user' );
- $input->usage_limit_per_user = $data['usage_limit_per_user'];
- }
- if ( array_key_exists( 'limit_usage_to_x_items', $data ) ) {
- $input->mark_provided( 'limit_usage_to_x_items' );
- $input->limit_usage_to_x_items = $data['limit_usage_to_x_items'];
- }
- if ( array_key_exists( 'free_shipping', $data ) ) {
- $input->mark_provided( 'free_shipping' );
- $input->free_shipping = $data['free_shipping'];
- }
- if ( array_key_exists( 'product_categories', $data ) ) {
- $input->mark_provided( 'product_categories' );
- $input->product_categories = $data['product_categories'];
- }
- if ( array_key_exists( 'excluded_product_categories', $data ) ) {
- $input->mark_provided( 'excluded_product_categories' );
- $input->excluded_product_categories = $data['excluded_product_categories'];
- }
- if ( array_key_exists( 'exclude_sale_items', $data ) ) {
- $input->mark_provided( 'exclude_sale_items' );
- $input->exclude_sale_items = $data['exclude_sale_items'];
- }
- if ( array_key_exists( 'minimum_amount', $data ) ) {
- $input->mark_provided( 'minimum_amount' );
- $input->minimum_amount = $data['minimum_amount'];
- }
- if ( array_key_exists( 'maximum_amount', $data ) ) {
- $input->mark_provided( 'maximum_amount' );
- $input->maximum_amount = $data['maximum_amount'];
- }
- if ( array_key_exists( 'email_restrictions', $data ) ) {
- $input->mark_provided( 'email_restrictions' );
- $input->email_restrictions = $data['email_restrictions'];
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php
deleted file mode 100644
index 2c89deac4d2..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Api\Mutations\Products\UpdateProduct as UpdateProductCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\UpdateProduct as UpdateProductInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class UpdateProduct {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( ProductInterface::get() ),
- 'description' => __( 'Update an existing product.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( UpdateProductInput::get() ),
- 'description' => __( 'The fields to update.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( UpdateProductCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_update_product_input( $args['input'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal );
- }
-
- private static function convert_dimensions_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput();
-
- if ( array_key_exists( 'length', $data ) ) {
- $input->mark_provided( 'length' );
- $input->length = $data['length'];
- }
- if ( array_key_exists( 'width', $data ) ) {
- $input->mark_provided( 'width' );
- $input->width = $data['width'];
- }
- if ( array_key_exists( 'height', $data ) ) {
- $input->mark_provided( 'height' );
- $input->height = $data['height'];
- }
- if ( array_key_exists( 'weight', $data ) ) {
- $input->mark_provided( 'weight' );
- $input->weight = $data['weight'];
- }
-
- return $input;
- }
-
- private static function convert_update_product_input( array $data ): \Automattic\WooCommerce\Api\InputTypes\Products\UpdateProductInput {
- $input = new \Automattic\WooCommerce\Api\InputTypes\Products\UpdateProductInput();
-
- if ( array_key_exists( 'id', $data ) ) {
- $input->mark_provided( 'id' );
- $input->id = $data['id'];
- }
- if ( array_key_exists( 'name', $data ) ) {
- $input->mark_provided( 'name' );
- $input->name = $data['name'];
- }
- if ( array_key_exists( 'slug', $data ) ) {
- $input->mark_provided( 'slug' );
- $input->slug = $data['slug'];
- }
- if ( array_key_exists( 'sku', $data ) ) {
- $input->mark_provided( 'sku' );
- $input->sku = $data['sku'];
- }
- if ( array_key_exists( 'description', $data ) ) {
- $input->mark_provided( 'description' );
- $input->description = $data['description'];
- }
- if ( array_key_exists( 'short_description', $data ) ) {
- $input->mark_provided( 'short_description' );
- $input->short_description = $data['short_description'];
- }
- if ( array_key_exists( 'status', $data ) ) {
- $input->mark_provided( 'status' );
- $input->status = $data['status'];
- }
- if ( array_key_exists( 'product_type', $data ) ) {
- $input->mark_provided( 'product_type' );
- $input->product_type = $data['product_type'];
- }
- if ( array_key_exists( 'regular_price', $data ) ) {
- $input->mark_provided( 'regular_price' );
- $input->regular_price = $data['regular_price'];
- }
- if ( array_key_exists( 'sale_price', $data ) ) {
- $input->mark_provided( 'sale_price' );
- $input->sale_price = $data['sale_price'];
- }
- if ( array_key_exists( 'manage_stock', $data ) ) {
- $input->mark_provided( 'manage_stock' );
- $input->manage_stock = $data['manage_stock'];
- }
- if ( array_key_exists( 'stock_quantity', $data ) ) {
- $input->mark_provided( 'stock_quantity' );
- $input->stock_quantity = $data['stock_quantity'];
- }
- if ( array_key_exists( 'dimensions', $data ) ) {
- $input->mark_provided( 'dimensions' );
- $input->dimensions = null !== $data['dimensions'] ? self::convert_dimensions_input( $data['dimensions'] ) : null;
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php
deleted file mode 100644
index 039ee59be8d..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Api\Queries\Coupons\GetCoupon as GetCouponCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class GetCoupon {
- public static function get_field_definition(): array {
- return array(
- 'type' => CouponType::get(),
- 'description' => __( 'Retrieve a single coupon by ID or code. Exactly one of the two arguments must be provided.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'read_private_shop_coupons',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::int(),
- 'description' => __( 'The ID of the coupon to retrieve.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'code' => array(
- 'type' => Type::string(),
- 'description' => __( 'The coupon code to look up.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( GetCouponCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
- if ( array_key_exists( 'code', $args ) ) {
- $execute_args['code'] = $args['code'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'read_private_shop_coupons' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php
deleted file mode 100644
index 9abc5643642..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Api\Queries\Products\GetProduct as GetProductCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class GetProduct {
- public static function get_field_definition(): array {
- return array(
- 'type' => ProductInterface::get(),
- 'description' => __( 'Retrieve a single product by ID.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'read_product',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the product to retrieve.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( GetProductCommand::class );
-
- $query_info = QueryInfoExtractor::extract_from_info( $info, $args );
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
- $execute_args['_query_info'] = $query_info;
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array(
- 'id' => $execute_args['id'],
- '_preauthorized' => self::compute_preauthorized( $context['principal'] ),
- )
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'read_product' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php
deleted file mode 100644
index 89b297a4d53..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Api\Queries\Coupons\ListCoupons as ListCouponsCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\CouponConnection as CouponConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ListCoupons {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( CouponConnectionType::get() ),
- 'description' => __( 'List coupons with cursor-based pagination.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'read_private_shop_coupons',
- ),
- ),
- ),
- 'args' => array(
- 'first' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the first N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'last' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the last N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'after' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results after this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'before' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results before this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'status' => array(
- 'type' => CouponStatusType::get(),
- 'description' => __( 'Filter by coupon status.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'complexity' => ResolverHelpers::complexity_from_pagination( ... ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( ListCouponsCommand::class );
-
- $execute_args = array();
- $execute_args['pagination'] = ResolverHelpers::create_pagination_params( $args );
- if ( array_key_exists( 'status', $args ) ) {
- $execute_args['status'] = $args['status'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'read_private_shop_coupons' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php
deleted file mode 100644
index b0c92452ff5..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Api\Queries\Products\ListProducts as ListProductsCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductConnection as ProductConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ListProducts {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( ProductConnectionType::get() ),
- 'description' => __( 'List products with cursor-based pagination and optional filtering.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'edit_products',
- ),
- ),
- ),
- 'args' => array(
- 'first' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the first N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'last' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the last N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'after' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results after this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'before' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results before this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'status' => array(
- 'type' => ProductStatusType::get(),
- 'description' => __( 'Filter by product status.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'stock_status' => array(
- 'type' => StockStatusType::get(),
- 'description' => __( 'Filter by stock status.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'search' => array(
- 'type' => Type::string(),
- 'description' => __( 'Search products by keyword.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'product_type' => array(
- 'type' => ProductTypeType::get(),
- 'description' => __( 'Filter by product type.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'complexity' => ResolverHelpers::complexity_from_pagination( ... ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Api\Infrastructure\ClassResolver::resolve_class( ListProductsCommand::class );
-
- $query_info = QueryInfoExtractor::extract_from_info( $info, $args );
- $execute_args = array();
- $execute_args['pagination'] = ResolverHelpers::create_pagination_params( $args );
- $execute_args['filters'] = ResolverHelpers::create_input(
- fn() => new \Automattic\WooCommerce\Api\InputTypes\Products\ProductFilterInput(
- status: $args['status'],
- stock_status: $args['stock_status'],
- search: $args['search'] ?? null,
- )
- );
- if ( array_key_exists( 'product_type', $args ) ) {
- $execute_args['product_type'] = $args['product_type'];
- }
- $execute_args['_query_info'] = $query_info;
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal ) && ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'edit_products' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php
deleted file mode 100644
index 69f9754f523..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus as CouponStatusEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class CouponStatus {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'CouponStatus',
- 'description' => __( 'The publication status of a coupon.', 'woocommerce' ),
- 'values' => array(
- 'PUBLISHED' => array(
- 'value' => CouponStatusEnum::Published,
- 'description' => __( 'The coupon is published and active.', 'woocommerce' ),
- ),
- 'DRAFT' => array(
- 'value' => CouponStatusEnum::Draft,
- 'description' => __( 'The coupon is a draft.', 'woocommerce' ),
- ),
- 'PENDING' => array(
- 'value' => CouponStatusEnum::Pending,
- 'description' => __( 'The coupon is pending review.', 'woocommerce' ),
- ),
- 'PRIVATE' => array(
- 'value' => CouponStatusEnum::Private,
- 'description' => __( 'The coupon is privately published.', 'woocommerce' ),
- ),
- 'FUTURE' => array(
- 'value' => CouponStatusEnum::Future,
- 'description' => __( 'The coupon is scheduled to be published in the future.', 'woocommerce' ),
- ),
- 'TRASH' => array(
- 'value' => CouponStatusEnum::Trash,
- 'description' => __( 'The coupon is in the trash.', 'woocommerce' ),
- ),
- 'OTHER' => array(
- 'value' => CouponStatusEnum::Other,
- 'description' => __( 'The coupon status is not one of the standard WordPress values (e.g. added by a plugin). Inspect raw_status for the underlying value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php
deleted file mode 100644
index 7cfb0ed3a61..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType as DiscountTypeEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class DiscountType {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'DiscountType',
- 'description' => __( 'The type of discount for a coupon.', 'woocommerce' ),
- 'values' => array(
- 'PERCENT' => array(
- 'value' => DiscountTypeEnum::Percent,
- 'description' => __( 'A percentage discount.', 'woocommerce' ),
- ),
- 'FIXED_CART' => array(
- 'value' => DiscountTypeEnum::FixedCart,
- 'description' => __( 'A fixed amount discount applied to the cart.', 'woocommerce' ),
- ),
- 'FIXED_PRODUCT' => array(
- 'value' => DiscountTypeEnum::FixedProduct,
- 'description' => __( 'A fixed amount discount applied to each eligible product.', 'woocommerce' ),
- ),
- 'OTHER' => array(
- 'value' => DiscountTypeEnum::Other,
- 'description' => __( 'The discount type is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_discount_type for the underlying value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php
deleted file mode 100644
index 544f2320a2c..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus as ProductStatusEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class ProductStatus {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'ProductStatus',
- 'description' => __( 'The publication status of a product.', 'woocommerce' ),
- 'values' => array(
- 'DRAFT' => array(
- 'value' => ProductStatusEnum::Draft,
- 'description' => __( 'The product is a draft.', 'woocommerce' ),
- ),
- 'PENDING' => array(
- 'value' => ProductStatusEnum::Pending,
- 'description' => __( 'The product is pending review.', 'woocommerce' ),
- ),
- 'ACTIVE' => array(
- 'value' => ProductStatusEnum::Published,
- 'description' => __( 'The product is published and visible.', 'woocommerce' ),
- ),
- 'PRIVATE' => array(
- 'value' => ProductStatusEnum::Private,
- 'description' => __( 'The product is privately published.', 'woocommerce' ),
- ),
- 'FUTURE' => array(
- 'value' => ProductStatusEnum::Future,
- 'description' => __( 'The product is scheduled to be published in the future.', 'woocommerce' ),
- ),
- 'TRASH' => array(
- 'value' => ProductStatusEnum::Trash,
- 'description' => __( 'The product is in the trash.', 'woocommerce' ),
- 'deprecationReason' => 'Trashed products should be excluded via status filter.',
- ),
- 'OTHER' => array(
- 'value' => ProductStatusEnum::Other,
- 'description' => __( 'The product status is not one of the standard WordPress values (e.g. added by a plugin). Inspect raw_status for the underlying value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php
deleted file mode 100644
index ef91dfe58f7..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Api\Enums\Products\ProductType as ProductTypeEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class ProductType {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'ProductType',
- 'description' => __( 'The type of a WooCommerce product.', 'woocommerce' ),
- 'values' => array(
- 'SIMPLE' => array(
- 'value' => ProductTypeEnum::Simple,
- 'description' => __( 'A simple product.', 'woocommerce' ),
- ),
- 'GROUPED' => array(
- 'value' => ProductTypeEnum::Grouped,
- 'description' => __( 'A grouped product.', 'woocommerce' ),
- ),
- 'EXTERNAL' => array(
- 'value' => ProductTypeEnum::External,
- 'description' => __( 'An external/affiliate product.', 'woocommerce' ),
- ),
- 'VARIABLE' => array(
- 'value' => ProductTypeEnum::Variable,
- 'description' => __( 'A variable product with variations.', 'woocommerce' ),
- ),
- 'VARIATION' => array(
- 'value' => ProductTypeEnum::Variation,
- 'description' => __( 'A product variation.', 'woocommerce' ),
- ),
- 'OTHER' => array(
- 'value' => ProductTypeEnum::Other,
- 'description' => __( 'The product type is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_product_type for the underlying value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php
deleted file mode 100644
index cf8915372a9..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus as StockStatusEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class StockStatus {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'StockStatus',
- 'description' => __( 'The stock status of a product.', 'woocommerce' ),
- 'values' => array(
- 'IN_STOCK' => array(
- 'value' => StockStatusEnum::InStock,
- 'description' => __( 'The product is in stock.', 'woocommerce' ),
- ),
- 'OUT_OF_STOCK' => array(
- 'value' => StockStatusEnum::OutOfStock,
- 'description' => __( 'The product is out of stock.', 'woocommerce' ),
- ),
- 'ON_BACKORDER' => array(
- 'value' => StockStatusEnum::OnBackorder,
- 'description' => __( 'The product is on backorder.', 'woocommerce' ),
- ),
- 'OTHER' => array(
- 'value' => StockStatusEnum::Other,
- 'description' => __( 'The stock status is not one of the standard WooCommerce values (e.g. added by a plugin). Inspect raw_stock_status for the underlying value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php
deleted file mode 100644
index 9328c7c6b14..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\DiscountType as DiscountTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateCoupon {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'CreateCouponInput',
- 'description' => __( 'Data required to create a new coupon.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'code' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The coupon code.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The coupon description.', 'woocommerce' ),
- ),
- 'discount_type' => array(
- 'type' => DiscountTypeType::get(),
- 'description' => __( 'The type of discount.', 'woocommerce' ),
- ),
- 'amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'The discount amount.', 'woocommerce' ),
- ),
- 'status' => array(
- 'type' => CouponStatusType::get(),
- 'description' => __( 'The coupon status.', 'woocommerce' ),
- ),
- 'date_expires' => array(
- 'type' => Type::string(),
- 'description' => __( 'The date the coupon expires (ISO 8601).', 'woocommerce' ),
- ),
- 'individual_use' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon can only be used alone.', 'woocommerce' ),
- ),
- 'product_ids' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product IDs the coupon can be applied to.', 'woocommerce' ),
- ),
- 'excluded_product_ids' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'usage_limit' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of times the coupon can be used in total.', 'woocommerce' ),
- ),
- 'usage_limit_per_user' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of times the coupon can be used per customer.', 'woocommerce' ),
- ),
- 'limit_usage_to_x_items' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of items the coupon can be applied to.', 'woocommerce' ),
- ),
- 'free_shipping' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon grants free shipping.', 'woocommerce' ),
- ),
- 'product_categories' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product category IDs the coupon applies to.', 'woocommerce' ),
- ),
- 'excluded_product_categories' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product category IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'exclude_sale_items' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon excludes items on sale.', 'woocommerce' ),
- ),
- 'minimum_amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'Minimum order amount required to use the coupon.', 'woocommerce' ),
- ),
- 'maximum_amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'Maximum order amount allowed to use the coupon.', 'woocommerce' ),
- ),
- 'email_restrictions' => array(
- 'type' => Type::listOf( Type::nonNull( Type::string() ) ),
- 'description' => __( 'Email addresses that can use this coupon.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php
deleted file mode 100644
index 2827b468ad5..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\Dimensions as DimensionsInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateProduct {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'CreateProductInput',
- 'description' => __( 'Data required to create a new product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- ),
- 'status' => array(
- 'type' => ProductStatusType::get(),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => ProductTypeType::get(),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::float(),
- 'description' => __( 'The regular price.', 'woocommerce' ),
- ),
- 'sale_price' => array(
- 'type' => Type::float(),
- 'description' => __( 'The sale price.', 'woocommerce' ),
- ),
- 'manage_stock' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether to manage stock.', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => DimensionsInput::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php
deleted file mode 100644
index 59a23161b44..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Dimensions {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'DimensionsInput',
- 'description' => __( 'Physical dimensions and weight for a product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'length' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product length.', 'woocommerce' ),
- ),
- 'width' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product width.', 'woocommerce' ),
- ),
- 'height' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product height.', 'woocommerce' ),
- ),
- 'weight' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product weight.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php
deleted file mode 100644
index cbfe22bfc6c..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductFilter {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'ProductFilterInput',
- 'description' => __( 'Filter criteria for listing products.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'status' => array(
- 'type' => ProductStatusType::get(),
- 'description' => __( 'Filter by product status.', 'woocommerce' ),
- ),
- 'stock_status' => array(
- 'type' => StockStatusType::get(),
- 'description' => __( 'Filter by stock status.', 'woocommerce' ),
- ),
- 'search' => array(
- 'type' => Type::string(),
- 'description' => __( 'Search products by keyword.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php
deleted file mode 100644
index f89460c73ee..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\DiscountType as DiscountTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class UpdateCoupon {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'UpdateCouponInput',
- 'description' => __( 'Data for updating an existing coupon. All fields are optional.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the coupon to update.', 'woocommerce' ),
- ),
- 'code' => array(
- 'type' => Type::string(),
- 'description' => __( 'The coupon code.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The coupon description.', 'woocommerce' ),
- ),
- 'discount_type' => array(
- 'type' => DiscountTypeType::get(),
- 'description' => __( 'The type of discount.', 'woocommerce' ),
- ),
- 'amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'The discount amount.', 'woocommerce' ),
- ),
- 'status' => array(
- 'type' => CouponStatusType::get(),
- 'description' => __( 'The coupon status.', 'woocommerce' ),
- ),
- 'date_expires' => array(
- 'type' => Type::string(),
- 'description' => __( 'The date the coupon expires (ISO 8601).', 'woocommerce' ),
- ),
- 'individual_use' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon can only be used alone.', 'woocommerce' ),
- ),
- 'product_ids' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product IDs the coupon can be applied to.', 'woocommerce' ),
- ),
- 'excluded_product_ids' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'usage_limit' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of times the coupon can be used in total.', 'woocommerce' ),
- ),
- 'usage_limit_per_user' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of times the coupon can be used per customer.', 'woocommerce' ),
- ),
- 'limit_usage_to_x_items' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of items the coupon can be applied to.', 'woocommerce' ),
- ),
- 'free_shipping' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon grants free shipping.', 'woocommerce' ),
- ),
- 'product_categories' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product category IDs the coupon applies to.', 'woocommerce' ),
- ),
- 'excluded_product_categories' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Product category IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'exclude_sale_items' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether the coupon excludes items on sale.', 'woocommerce' ),
- ),
- 'minimum_amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'Minimum order amount required to use the coupon.', 'woocommerce' ),
- ),
- 'maximum_amount' => array(
- 'type' => Type::float(),
- 'description' => __( 'Maximum order amount allowed to use the coupon.', 'woocommerce' ),
- ),
- 'email_restrictions' => array(
- 'type' => Type::listOf( Type::nonNull( Type::string() ) ),
- 'description' => __( 'Email addresses that can use this coupon.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php
deleted file mode 100644
index a0f8a8fff6f..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\Dimensions as DimensionsInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class UpdateProduct {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'UpdateProductInput',
- 'description' => __( 'Data for updating an existing product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the product to update.', 'woocommerce' ),
- ),
- 'name' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::string(),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- ),
- 'status' => array(
- 'type' => ProductStatusType::get(),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => ProductTypeType::get(),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::float(),
- 'description' => __( 'The regular price.', 'woocommerce' ),
- ),
- 'sale_price' => array(
- 'type' => Type::float(),
- 'description' => __( 'The sale price.', 'woocommerce' ),
- ),
- 'manage_stock' => array(
- 'type' => Type::boolean(),
- 'description' => __( 'Whether to manage stock.', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => DimensionsInput::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php
deleted file mode 100644
index dbab128006b..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ObjectWithId {
- private static ?InterfaceType $instance = null;
-
- public static function get(): InterfaceType {
- if ( null === self::$instance ) {
- self::$instance = new InterfaceType(
- array(
- 'name' => 'ObjectWithId',
- 'description' => __( 'An object with a numeric ID.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- 'resolveType' => function ( $value ) {
- $class = get_class( $value );
- $map = array(
- 'Automattic\WooCommerce\Api\Types\Coupons\Coupon' => CouponType::get(),
- );
- return $map[ $class ] ?? null;
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php
deleted file mode 100644
index 8e76923165a..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductDimensions;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductImage;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation as ProductVariationType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ExternalProduct as ExternalProductType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\VariableProduct as VariableProductType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\SimpleProduct as SimpleProductType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Product {
- private static ?InterfaceType $instance = null;
-
- public static function get(): InterfaceType {
- if ( null === self::$instance ) {
- self::$instance = new InterfaceType(
- array(
- 'name' => 'Product',
- 'description' => __( 'A WooCommerce product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- 'deprecationReason' => 'Use description instead.',
- ),
- 'status' => array(
- 'type' => Type::nonNull( ProductStatusType::get() ),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => Type::nonNull( ProductTypeType::get() ),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'raw_product_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The regular price of the product. Null when not set.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'Whether to apply currency formatting.', 'woocommerce' ),
- ),
- ),
- ),
- 'sale_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The sale price of the product.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'When true, returns price with currency symbol.', 'woocommerce' ),
- ),
- ),
- ),
- 'stock_status' => array(
- 'type' => Type::nonNull( StockStatusType::get() ),
- 'description' => __( 'The stock status of the product.', 'woocommerce' ),
- ),
- 'raw_stock_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => ProductDimensions::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- 'images' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductImage::get() ) ) ),
- 'description' => __( 'The product images.', 'woocommerce' ),
- ),
- 'attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductAttribute::get() ) ) ),
- 'description' => __( 'The product attributes.', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( ProductReviewConnectionType::get() ),
- 'description' => __( 'Customer reviews for this product.', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was last modified.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- 'resolveType' => function ( $value ) {
- $class = get_class( $value );
- $map = array(
- 'Automattic\WooCommerce\Api\Types\Products\ProductVariation' => ProductVariationType::get(),
- 'Automattic\WooCommerce\Api\Types\Products\ExternalProduct' => ExternalProductType::get(),
- 'Automattic\WooCommerce\Api\Types\Products\VariableProduct' => VariableProductType::get(),
- 'Automattic\WooCommerce\Api\Types\Products\SimpleProduct' => SimpleProductType::get(),
- );
- return $map[ $class ] ?? null;
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php
deleted file mode 100644
index 2f081e6fe1c..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\DiscountType as DiscountTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\ObjectWithId as ObjectWithIdInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Coupon {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Coupon',
- 'description' => __( 'Represents a WooCommerce discount coupon.', 'woocommerce' ),
- 'interfaces' => fn() => array(
- ObjectWithIdInterface::get(),
- ),
- 'fields' => fn() => array(
- 'code' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The coupon code.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The coupon description.', 'woocommerce' ),
- ),
- 'discount_type' => array(
- 'type' => Type::nonNull( DiscountTypeType::get() ),
- 'description' => __( 'The type of discount.', 'woocommerce' ),
- ),
- 'raw_discount_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw discount type as stored in WooCommerce. Useful when discount_type is OTHER (e.g. plugin-added types like recurring_percent or sign_up_fee).', 'woocommerce' ),
- ),
- 'amount' => array(
- 'type' => Type::nonNull( Type::float() ),
- 'description' => __( 'The discount amount.', 'woocommerce' ),
- ),
- 'status' => array(
- 'type' => Type::nonNull( CouponStatusType::get() ),
- 'description' => __( 'The coupon status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the coupon was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the coupon was last modified.', 'woocommerce' ),
- ),
- 'date_expires' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the coupon expires.', 'woocommerce' ),
- ),
- 'usage_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The number of times the coupon has been used.', 'woocommerce' ),
- ),
- 'individual_use' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the coupon can only be used alone.', 'woocommerce' ),
- ),
- 'product_ids' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) ),
- 'description' => __( 'Product IDs the coupon can be applied to.', 'woocommerce' ),
- ),
- 'excluded_product_ids' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) ),
- 'description' => __( 'Product IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'usage_limit' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'Maximum number of times the coupon can be used in total.', 'woocommerce' ),
- ),
- 'usage_limit_per_user' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'Maximum number of times the coupon can be used per customer.', 'woocommerce' ),
- ),
- 'limit_usage_to_x_items' => array(
- 'type' => Type::int(),
- 'description' => __( 'Maximum number of items the coupon can be applied to.', 'woocommerce' ),
- ),
- 'free_shipping' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the coupon grants free shipping.', 'woocommerce' ),
- ),
- 'product_categories' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) ),
- 'description' => __( 'Product category IDs the coupon applies to.', 'woocommerce' ),
- ),
- 'excluded_product_categories' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) ),
- 'description' => __( 'Product category IDs excluded from the coupon.', 'woocommerce' ),
- ),
- 'exclude_sale_items' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the coupon excludes items on sale.', 'woocommerce' ),
- ),
- 'minimum_amount' => array(
- 'type' => Type::nonNull( Type::float() ),
- 'description' => __( 'Minimum order amount required to use the coupon.', 'woocommerce' ),
- ),
- 'maximum_amount' => array(
- 'type' => Type::nonNull( Type::float() ),
- 'description' => __( 'Maximum order amount allowed to use the coupon.', 'woocommerce' ),
- ),
- 'email_restrictions' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::string() ) ) ),
- 'description' => __( 'Email addresses that can use this coupon.', 'woocommerce' ),
- ),
- 'used_by' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::string() ) ) ),
- 'description' => __( 'Email addresses of customers who have used this coupon.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php
deleted file mode 100644
index 4a9a2da709f..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class DeleteCouponResult {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'DeleteCouponResult',
- 'description' => __( 'The result of deleting a coupon.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the deleted coupon.', 'woocommerce' ),
- ),
- 'deleted' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the coupon was permanently deleted.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php
deleted file mode 100644
index ae28fa1230c..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductDimensions;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductImage;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ExternalProduct {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ExternalProduct',
- 'description' => __( 'An external/affiliate product.', 'woocommerce' ),
- 'interfaces' => fn() => array(
- ProductInterface::get(),
- ),
- 'fields' => fn() => array(
- 'product_url' => array(
- 'type' => Type::string(),
- 'description' => __( 'The external product URL.', 'woocommerce' ),
- ),
- 'button_text' => array(
- 'type' => Type::string(),
- 'description' => __( 'The text for the external product button.', 'woocommerce' ),
- ),
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- 'deprecationReason' => 'Use description instead.',
- ),
- 'status' => array(
- 'type' => Type::nonNull( ProductStatusType::get() ),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => Type::nonNull( ProductTypeType::get() ),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'raw_product_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The regular price of the product. Null when not set.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'Whether to apply currency formatting.', 'woocommerce' ),
- ),
- ),
- ),
- 'sale_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The sale price of the product.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'When true, returns price with currency symbol.', 'woocommerce' ),
- ),
- ),
- ),
- 'stock_status' => array(
- 'type' => Type::nonNull( StockStatusType::get() ),
- 'description' => __( 'The stock status of the product.', 'woocommerce' ),
- ),
- 'raw_stock_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => ProductDimensions::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- 'images' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductImage::get() ) ) ),
- 'description' => __( 'The product images.', 'woocommerce' ),
- ),
- 'attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductAttribute::get() ) ) ),
- 'description' => __( 'The product attributes.', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( ProductReviewConnectionType::get() ),
- 'description' => __( 'Customer reviews for this product.', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was last modified.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php
deleted file mode 100644
index 6b66b67d9b7..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductAttribute {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductAttribute',
- 'description' => __( 'A product attribute.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The attribute display name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The attribute taxonomy or key name.', 'woocommerce' ),
- ),
- 'options' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::string() ) ) ),
- 'description' => __( 'The available attribute values.', 'woocommerce' ),
- ),
- 'position' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The display order position.', 'woocommerce' ),
- ),
- 'visible' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the attribute is visible on the product page.', 'woocommerce' ),
- ),
- 'variation' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the attribute is used for variations.', 'woocommerce' ),
- ),
- 'is_taxonomy' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the attribute is a global taxonomy attribute.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php
deleted file mode 100644
index 350df969ab7..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductDimensions {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductDimensions',
- 'description' => __( 'Physical dimensions and weight of a product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'length' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product length.', 'woocommerce' ),
- ),
- 'width' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product width.', 'woocommerce' ),
- ),
- 'height' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product height.', 'woocommerce' ),
- ),
- 'weight' => array(
- 'type' => Type::float(),
- 'description' => __( 'The product weight.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php
deleted file mode 100644
index 5ace0e2b030..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductImage {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductImage',
- 'description' => __( 'Represents a product image.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The image attachment ID.', 'woocommerce' ),
- ),
- 'url' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The image URL.', 'woocommerce' ),
- ),
- 'alt' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The image alt text.', 'woocommerce' ),
- ),
- 'position' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The image display position.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php
deleted file mode 100644
index 00189e5dbd8..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductReview {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductReview',
- 'description' => __( 'Represents a customer review for a product.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The review ID.', 'woocommerce' ),
- ),
- 'product_id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The product ID this review belongs to.', 'woocommerce' ),
- ),
- 'reviewer' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The reviewer name.', 'woocommerce' ),
- ),
- 'review' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The review content.', 'woocommerce' ),
- ),
- 'rating' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The review rating (1-5).', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the review was created.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php
deleted file mode 100644
index dbfdd841631..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\SelectedAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductDimensions;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductImage;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductVariation {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductVariation',
- 'description' => __( 'A product variation.', 'woocommerce' ),
- 'interfaces' => fn() => array(
- ProductInterface::get(),
- ),
- 'fields' => fn() => array(
- 'parent_id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The parent variable product ID.', 'woocommerce' ),
- ),
- 'selected_attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( SelectedAttribute::get() ) ) ),
- 'description' => __( 'The selected attribute values for this variation.', 'woocommerce' ),
- ),
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- 'deprecationReason' => 'Use description instead.',
- ),
- 'status' => array(
- 'type' => Type::nonNull( ProductStatusType::get() ),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => Type::nonNull( ProductTypeType::get() ),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'raw_product_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The regular price of the product. Null when not set.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'Whether to apply currency formatting.', 'woocommerce' ),
- ),
- ),
- ),
- 'sale_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The sale price of the product.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'When true, returns price with currency symbol.', 'woocommerce' ),
- ),
- ),
- ),
- 'stock_status' => array(
- 'type' => Type::nonNull( StockStatusType::get() ),
- 'description' => __( 'The stock status of the product.', 'woocommerce' ),
- ),
- 'raw_stock_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => ProductDimensions::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- 'images' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductImage::get() ) ) ),
- 'description' => __( 'The product images.', 'woocommerce' ),
- ),
- 'attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductAttribute::get() ) ) ),
- 'description' => __( 'The product attributes.', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( ProductReviewConnectionType::get() ),
- 'description' => __( 'Customer reviews for this product.', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was last modified.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php
deleted file mode 100644
index 93e9b56f70e..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class SelectedAttribute {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'SelectedAttribute',
- 'description' => __( 'A selected attribute value on a product variation.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The attribute name or slug.', 'woocommerce' ),
- ),
- 'value' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The selected attribute value.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php
deleted file mode 100644
index 9a5c11ed5bf..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductDimensions;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductImage;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class SimpleProduct {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'SimpleProduct',
- 'description' => __( 'A simple WooCommerce product.', 'woocommerce' ),
- 'interfaces' => fn() => array(
- ProductInterface::get(),
- ),
- 'fields' => fn() => array(
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- 'deprecationReason' => 'Use description instead.',
- ),
- 'status' => array(
- 'type' => Type::nonNull( ProductStatusType::get() ),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => Type::nonNull( ProductTypeType::get() ),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'raw_product_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The regular price of the product. Null when not set.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'Whether to apply currency formatting.', 'woocommerce' ),
- ),
- ),
- ),
- 'sale_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The sale price of the product.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'When true, returns price with currency symbol.', 'woocommerce' ),
- ),
- ),
- ),
- 'stock_status' => array(
- 'type' => Type::nonNull( StockStatusType::get() ),
- 'description' => __( 'The stock status of the product.', 'woocommerce' ),
- ),
- 'raw_stock_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => ProductDimensions::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- 'images' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductImage::get() ) ) ),
- 'description' => __( 'The product images.', 'woocommerce' ),
- ),
- 'attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductAttribute::get() ) ) ),
- 'description' => __( 'The product attributes.', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( ProductReviewConnectionType::get() ),
- 'description' => __( 'Customer reviews for this product.', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was last modified.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php
deleted file mode 100644
index 1c1832b30e3..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php
+++ /dev/null
@@ -1,169 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductVariationConnection as ProductVariationConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductDimensions;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductImage;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductAttribute;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class VariableProduct {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'VariableProduct',
- 'description' => __( 'A variable product with variations.', 'woocommerce' ),
- 'interfaces' => fn() => array(
- ProductInterface::get(),
- ),
- 'fields' => fn() => array(
- 'variations' => array(
- 'type' => Type::nonNull( ProductVariationConnectionType::get() ),
- 'description' => __( 'The product variations.', 'woocommerce' ),
- 'args' => array(
- 'first' => array(
- 'type' => Type::int(),
- 'defaultValue' => null,
- 'description' => __( 'Return the first N results. Must be between 0 and 100.', 'woocommerce' ),
- ),
- 'last' => array(
- 'type' => Type::int(),
- 'defaultValue' => null,
- 'description' => __( 'Return the last N results. Must be between 0 and 100.', 'woocommerce' ),
- ),
- 'after' => array(
- 'type' => Type::string(),
- 'defaultValue' => null,
- 'description' => __( 'Return results after this cursor.', 'woocommerce' ),
- ),
- 'before' => array(
- 'type' => Type::string(),
- 'defaultValue' => null,
- 'description' => __( 'Return results before this cursor.', 'woocommerce' ),
- ),
- ),
- 'complexity' => ResolverHelpers::complexity_from_pagination( ... ),
- 'resolve' => fn( $parent, array $args ): Connection => ResolverHelpers::translate_exceptions( fn() => $parent->variations->slice( $args ) ),
- ),
- 'name' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product name.', 'woocommerce' ),
- ),
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The product slug.', 'woocommerce' ),
- ),
- 'sku' => array(
- 'type' => Type::string(),
- 'description' => __( 'The product SKU.', 'woocommerce' ),
- ),
- 'description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The full product description.', 'woocommerce' ),
- ),
- 'short_description' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The short product description.', 'woocommerce' ),
- 'deprecationReason' => 'Use description instead.',
- ),
- 'status' => array(
- 'type' => Type::nonNull( ProductStatusType::get() ),
- 'description' => __( 'The product status.', 'woocommerce' ),
- ),
- 'raw_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw status as stored in WordPress. Useful when status is OTHER (e.g. plugin-added post statuses).', 'woocommerce' ),
- ),
- 'product_type' => array(
- 'type' => Type::nonNull( ProductTypeType::get() ),
- 'description' => __( 'The product type.', 'woocommerce' ),
- ),
- 'raw_product_type' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw product type as stored in WooCommerce. Useful when product_type is OTHER (e.g. plugin-added types like subscription, bundle).', 'woocommerce' ),
- ),
- 'regular_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The regular price of the product. Null when not set.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'Whether to apply currency formatting.', 'woocommerce' ),
- ),
- ),
- ),
- 'sale_price' => array(
- 'type' => Type::string(),
- 'description' => __( 'The sale price of the product.', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => true,
- 'description' => __( 'When true, returns price with currency symbol.', 'woocommerce' ),
- ),
- ),
- ),
- 'stock_status' => array(
- 'type' => Type::nonNull( StockStatusType::get() ),
- 'description' => __( 'The stock status of the product.', 'woocommerce' ),
- ),
- 'raw_stock_status' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The raw stock status as stored in WooCommerce. Useful when stock_status is OTHER (e.g. plugin-added statuses).', 'woocommerce' ),
- ),
- 'stock_quantity' => array(
- 'type' => Type::int(),
- 'description' => __( 'The number of items in stock.', 'woocommerce' ),
- ),
- 'dimensions' => array(
- 'type' => ProductDimensions::get(),
- 'description' => __( 'The product dimensions.', 'woocommerce' ),
- ),
- 'images' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductImage::get() ) ) ),
- 'description' => __( 'The product images.', 'woocommerce' ),
- ),
- 'attributes' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( ProductAttribute::get() ) ) ),
- 'description' => __( 'The product attributes.', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( ProductReviewConnectionType::get() ),
- 'description' => __( 'Customer reviews for this product.', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was created.', 'woocommerce' ),
- ),
- 'date_modified' => array(
- 'type' => DateTimeType::get(),
- 'description' => __( 'The date the product was last modified.', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier.', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php
deleted file mode 100644
index 08fbabc9594..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CouponConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'CouponConnection',
- 'description' => __( 'A connection to a list of Coupon items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- CouponEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- CouponType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php
deleted file mode 100644
index 5598e77bf11..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CouponEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'CouponEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( CouponType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php
deleted file mode 100644
index a6ca8f7ea47..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PageInfo {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'PageInfo',
- 'fields' => array(
- 'has_next_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'has_previous_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'start_cursor' => array(
- 'type' => Type::string(),
- ),
- 'end_cursor' => array(
- 'type' => Type::string(),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php
deleted file mode 100644
index e8b61c2b1ec..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductConnection',
- 'description' => __( 'A connection to a list of Product items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php
deleted file mode 100644
index 337fd9a5674..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( ProductType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php
deleted file mode 100644
index 94c79312124..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductReview as ProductReviewType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductReviewConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductReviewConnection',
- 'description' => __( 'A connection to a list of ProductReview items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductReviewEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductReviewType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php
deleted file mode 100644
index 23a5ecc1bcd..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductReview as ProductReviewType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductReviewEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductReviewEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( ProductReviewType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php
deleted file mode 100644
index 3a75c4bfe99..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation as ProductVariationType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductVariationConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductVariationConnection',
- 'description' => __( 'A connection to a list of ProductVariation items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductVariationEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- ProductVariationType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php
deleted file mode 100644
index e68e1606a07..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation as ProductVariationType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ProductVariationEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'ProductVariationEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( ProductVariationType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php
deleted file mode 100644
index 223d835e695..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars;
-
-use Automattic\WooCommerce\Api\Scalars\DateTime as DateTimeScalar;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-
-class DateTime {
- private static ?CustomScalarType $instance = null;
-
- public static function get(): CustomScalarType {
- if ( null === self::$instance ) {
- self::$instance = new CustomScalarType(
- array(
- 'name' => 'DateTime',
- 'description' => __( 'An ISO 8601 encoded date and time string.', 'woocommerce' ),
- 'serialize' => fn( $value ) => DateTimeScalar::serialize( $value ),
- 'parseValue' => function ( $value ) {
- try {
- return DateTimeScalar::parse( $value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- },
- 'parseLiteral' => function ( $value_node, ?array $variables = null ) {
- if ( $value_node instanceof \Automattic\WooCommerce\Api\Infrastructure\Schema\AST\StringValueNode ) {
- try {
- return DateTimeScalar::parse( $value_node->value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- }
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error(
- 'DateTime must be a string, got: ' . $value_node->kind
- );
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php
deleted file mode 100644
index c2d27b4712e..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\CreateProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\UpdateProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\DeleteProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\DeleteCoupon;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\CreateCoupon;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\UpdateCoupon;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootMutationType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Mutation',
- 'fields' => fn() => array(
- 'createProduct' => CreateProduct::get_field_definition(),
- 'updateProduct' => UpdateProduct::get_field_definition(),
- 'deleteProduct' => DeleteProduct::get_field_definition(),
- 'deleteCoupon' => DeleteCoupon::get_field_definition(),
- 'createCoupon' => CreateCoupon::get_field_definition(),
- 'updateCoupon' => UpdateCoupon::get_field_definition(),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php
deleted file mode 100644
index 7e2724af54e..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\ListProducts;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\GetProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\GetCoupon;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\ListCoupons;
-use Automattic\WooCommerce\Api\Infrastructure\MetadataController;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootQueryType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => fn() => array(
- 'products' => ListProducts::get_field_definition(),
- 'product' => GetProduct::get_field_definition(),
- 'coupon' => GetCoupon::get_field_definition(),
- 'coupons' => ListCoupons::get_field_definition(),
- MetadataController::FIELD_NAME => MetadataController::get_field_definition(),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/TypeRegistry.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/TypeRegistry.php
deleted file mode 100644
index c8ae533c2a3..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/TypeRegistry.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ExternalProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\VariableProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\SimpleProduct;
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon;
-
-class TypeRegistry {
- /**
- * Return all concrete types that implement interfaces.
- *
- * Pass this to the Schema 'types' config so that inline fragments
- * (e.g. `... on VariableProduct`) are resolvable.
- *
- * @return array
- */
- public static function get_interface_implementors(): array {
- return array(
- ProductVariation::get(),
- ExternalProduct::get(),
- VariableProduct::get(),
- SimpleProduct::get(),
- Coupon::get(),
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt b/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt
deleted file mode 100644
index 888b1f56ac5..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt
+++ /dev/null
@@ -1 +0,0 @@
-2026-09-01T14:24:30+00:00
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_source_hash.txt b/plugins/woocommerce/src/Internal/Api/Autogenerated/api_source_hash.txt
deleted file mode 100644
index 1326460d955..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_source_hash.txt
+++ /dev/null
@@ -1 +0,0 @@
-8a1772a0ce7165390f95a55b811332ed47a895440da445291275295d06089e70
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Api/GraphQLEndpointRegistrar.php b/plugins/woocommerce/src/Internal/Api/GraphQLEndpointRegistrar.php
deleted file mode 100644
index 94e776f16cf..00000000000
--- a/plugins/woocommerce/src/Internal/Api/GraphQLEndpointRegistrar.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-
-/**
- * Deferred-registration helper for GraphQL endpoints declared via
- * {@see Main::register_graphql_endpoint()}.
- *
- * Each instance captures the arguments of a single registration request and
- * exposes {@see self::handle_rest_api_init()} as the callback target for the
- * rest_api_init action, so Main doesn't have to carry per-registration state
- * through a closure.
- */
-class GraphQLEndpointRegistrar {
- /**
- * Capture the arguments of a single register_graphql_endpoint() call.
- *
- * @param string $controller_class_name Fully-qualified name of a concrete GraphQLController subclass.
- * @param string $route_namespace REST namespace passed to register_rest_route().
- * @param string $route REST route path passed to register_rest_route().
- * @param string[] $methods HTTP methods accepted on the endpoint.
- */
- public function __construct(
- private readonly string $controller_class_name,
- private readonly string $route_namespace,
- private readonly string $route,
- private readonly array $methods
- ) {}
-
- /**
- * Hook callback for rest_api_init. Instantiates the controller and
- * registers the REST route.
- *
- * The caller-declared methods are narrowed by
- * {@see Main::filter_methods_against_settings()} so plugin endpoints honour
- * the same site-wide settings (e.g. the GET-endpoint toggle) as
- * WooCommerce core's `/wc/graphql`. If the filter empties the list the
- * endpoint is not registered.
- */
- public function handle_rest_api_init(): void {
- $methods = Main::filter_methods_against_settings( $this->methods );
- if ( empty( $methods ) ) {
- return;
- }
-
- $controller = Main::instantiate_graphql_controller( $this->controller_class_name );
- if ( null === $controller ) {
- return;
- }
-
- register_rest_route(
- $this->route_namespace,
- $this->route,
- array(
- 'methods' => $methods,
- 'callback' => array( $controller, 'handle_request' ),
- // Auth is handled per-query/mutation.
- 'permission_callback' => '__return_true',
- )
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/OpcacheFileExpiry.php b/plugins/woocommerce/src/Internal/Api/OpcacheFileExpiry.php
deleted file mode 100644
index e115cec8d89..00000000000
--- a/plugins/woocommerce/src/Internal/Api/OpcacheFileExpiry.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-
-/**
- * Deletes expired OPcache cache files via Action Scheduler.
- */
-class OpcacheFileExpiry {
-
- /**
- * Action Scheduler hook name for the cleanup job.
- */
- public const ACTION_HOOK = 'woocommerce_graphql_opcache_cleanup';
-
- /**
- * Action Scheduler group for the cleanup job.
- */
- public const ACTION_GROUP = 'woocommerce-graphql';
-
- /**
- * Object-cache key used to short-circuit {@see self::ensure_scheduled()}.
- */
- private const SCHEDULED_CACHE_KEY = 'graphql_opcache_cleanup_scheduled';
-
- /**
- * Delete OPcache cache files older than {@see QueryCache::get_cache_ttl()}.
- *
- * AST contents are a pure function of the query, so this is a disk-usage
- * bound, not a correctness concern. Returns the count.
- */
- public static function delete_expired_files(): int {
- $dir = QueryCache::get_opcache_cache_dir();
- if ( '' === $dir || ! is_dir( $dir ) ) {
- return 0;
- }
-
- $fs = ResolverHelpers::wp_filesystem();
- if ( ! $fs ) {
- return 0;
- }
-
- $files = glob( $dir . '/*.php' );
- if ( false === $files ) {
- return 0;
- }
-
- $cutoff = time() - QueryCache::get_cache_ttl();
- $count = 0;
- foreach ( $files as $path ) {
- $mtime = $fs->mtime( $path );
- if ( false !== $mtime && $mtime < $cutoff && $fs->delete( $path ) ) {
- ++$count;
- }
- }
-
- return $count;
- }
-
- /**
- * Action Scheduler callback: delete expired files and reschedule.
- *
- * Immediate reschedule when files were deleted (drain the backlog), 24h
- * otherwise. Skipped when the feature is disabled.
- *
- * @internal
- */
- public static function handle_cleanup_action(): void {
- $interval = self::delete_expired_files() > 0 ? 1 : DAY_IN_SECONDS;
-
- if ( ! Main::is_enabled() ) {
- return;
- }
-
- if ( function_exists( 'as_schedule_single_action' ) ) {
- as_schedule_single_action( time() + $interval, self::ACTION_HOOK, array(), self::ACTION_GROUP );
- }
- }
-
- /**
- * Schedule the cleanup if it isn't already scheduled.
- *
- * Called from {@see QueryCache::write_to_opcache()} and
- * {@see QueryCache::read_from_opcache()} so the cleanup is rescheduled
- * after a feature-disable/re-enable cycle even when every request hits a
- * cached file (no writes).
- */
- public static function ensure_scheduled(): void {
- if ( wp_cache_get( self::SCHEDULED_CACHE_KEY, QueryCache::CACHE_GROUP ) ) {
- return;
- }
-
- if ( ! function_exists( 'as_has_scheduled_action' ) || ! function_exists( 'as_schedule_single_action' ) ) {
- return;
- }
-
- if ( ! as_has_scheduled_action( self::ACTION_HOOK, array(), self::ACTION_GROUP ) ) {
- as_schedule_single_action( time() + DAY_IN_SECONDS, self::ACTION_HOOK, array(), self::ACTION_GROUP );
- }
-
- wp_cache_set( self::SCHEDULED_CACHE_KEY, true, QueryCache::CACHE_GROUP, HOUR_IN_SECONDS );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/QueryCache.php b/plugins/woocommerce/src/Internal/Api/QueryCache.php
deleted file mode 100644
index 2d9d33a40c5..00000000000
--- a/plugins/woocommerce/src/Internal/Api/QueryCache.php
+++ /dev/null
@@ -1,443 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
-
-/**
- * Caches parsed GraphQL ASTs and implements the Apollo Automatic Persisted
- * Queries (APQ) protocol.
- *
- * Two backends are supported. OPcache (filesystem) is preferred: parsed ASTs
- * are written as PHP files so that OPcache serves them from shared memory.
- * The WP object cache is used as a fallback when OPcache isn't available or
- * the cache directory isn't writable.
- */
-class QueryCache {
- /**
- * WP object-cache group.
- */
- public const CACHE_GROUP = 'wc-graphql';
-
- /**
- * Cache key prefix. Includes the library major version so that upgrading
- * webonyx/graphql-php naturally invalidates stale entries.
- *
- * Update this constant when bumping the major version in composer.json.
- */
- private const CACHE_KEY_PREFIX = 'graphql_ast_v15_';
-
- /**
- * Subdirectory (under wp-uploads) for the OPcache-backed file cache.
- * The version segment matches {@see self::CACHE_KEY_PREFIX} so a major
- * webonyx upgrade naturally orphans the previous version's files.
- */
- private const OPCACHE_DIR_RELATIVE = 'wc-graphql-cache/v15';
-
- /**
- * Cached result of {@see self::is_opcache_usable()} for the current request.
- *
- * @var ?bool
- */
- private ?bool $opcache_usable = null;
-
- /**
- * Default time-to-live (in seconds) applied when the option is unset or non-positive.
- *
- * See {@see self::get_cache_ttl()} for the accessor.
- */
- public const DEFAULT_CACHE_TTL = DAY_IN_SECONDS;
-
- /**
- * The time-to-live (in seconds) for a cached parsed query.
- *
- * Reads the {@see Main::OPTION_QUERY_CACHE_TTL} store option; falls back
- * to {@see self::DEFAULT_CACHE_TTL} when the option is unset, empty, or
- * non-positive.
- */
- public static function get_cache_ttl(): int {
- $value = (int) get_option( Main::OPTION_QUERY_CACHE_TTL, self::DEFAULT_CACHE_TTL );
- return $value > 0 ? $value : self::DEFAULT_CACHE_TTL;
- }
-
- /**
- * Resolve a query string (and optional APQ extensions) into a DocumentNode.
- *
- * Returns a DocumentNode on success, or a GraphQL-shaped error array on failure.
- *
- * @param ?string $query The GraphQL query string (may be null for APQ hash-only requests).
- * @param array $extensions The request extensions (may contain persistedQuery).
- * @return DocumentNode|array
- */
- public function resolve( ?string $query, array $extensions ) {
- $apq = $extensions['persistedQuery'] ?? null;
- $apq_hash = is_array( $apq ) ? ( $apq['sha256Hash'] ?? null ) : null;
-
- if ( Main::is_apq_enabled()
- && is_array( $apq )
- && 1 === ( $apq['version'] ?? null )
- && is_string( $apq_hash )
- && 1 === preg_match( '/^[a-f0-9]{64}$/', $apq_hash ) ) {
- return $this->resolve_apq( $query, $apq_hash );
- }
-
- // Standard query — no APQ.
- if ( empty( $query ) ) {
- return $this->error_response( 'No query provided.', 'BAD_REQUEST' );
- }
-
- // APQ keeps using the cache; it has its own settings toggle.
- if ( ! $this->is_caching_enabled() ) {
- return $this->parse( $query );
- }
-
- $hash = hash( 'sha256', $query );
- $doc = $this->get_cached_document( $hash );
- if ( false !== $doc ) {
- return $doc;
- }
-
- return $this->parse_and_cache( $query, $hash );
- }
-
- /**
- * Handle an APQ request (hash present in extensions).
- *
- * @param ?string $query The query string, if provided.
- * @param string $apq_hash The sha256 hash from the persistedQuery extension.
- * @return DocumentNode|array
- */
- private function resolve_apq( ?string $query, string $apq_hash ) {
- if ( ! empty( $query ) ) {
- // Registration: query + hash provided.
- if ( hash( 'sha256', $query ) !== $apq_hash ) {
- return $this->error_response(
- 'provided sha does not match query',
- 'PERSISTED_QUERY_HASH_MISMATCH'
- );
- }
-
- $doc = $this->get_cached_document( $apq_hash, true );
- if ( false !== $doc ) {
- return $doc;
- }
-
- return $this->parse_and_cache( $query, $apq_hash, true );
- }
-
- // Hash-only lookup.
- $doc = $this->get_cached_document( $apq_hash, true );
- if ( false !== $doc ) {
- return $doc;
- }
-
- return $this->error_response( 'PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND' );
- }
-
- /**
- * Whether at least one cache backend is enabled (and, for OPcache, usable).
- *
- * Used to short-circuit the standard-query path when neither backend is
- * available, so the request is parsed once with no cache lookup overhead.
- */
- private function is_caching_enabled(): bool {
- return ( Main::is_opcache_enabled() && $this->is_opcache_usable() )
- || Main::is_object_cache_enabled();
- }
-
- /**
- * Retrieve a cached DocumentNode by hash.
- *
- * Tries OPcache first when enabled and usable, then falls back to the
- * WP object cache. APQ requests pass $for_apq=true so the object cache
- * is consulted regardless of the standard-query toggle, matching the
- * pre-OPcache behaviour where APQ always persisted via the object cache.
- *
- * @param string $hash The SHA-256 hash.
- * @param bool $for_apq Whether the lookup is for an APQ request.
- * @return DocumentNode|false
- */
- private function get_cached_document( string $hash, bool $for_apq = false ) {
- if ( Main::is_opcache_enabled() && $this->is_opcache_usable() ) {
- $doc = $this->read_from_opcache( $hash );
- if ( false !== $doc ) {
- return $doc;
- }
- }
-
- if ( $for_apq || Main::is_object_cache_enabled() ) {
- $cached = wp_cache_get( $this->build_cache_key( $hash ), self::CACHE_GROUP );
- if ( is_array( $cached ) ) {
- try {
- return AST::fromArray( $cached );
- } catch ( \Throwable $e ) {
- return false;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Parse a query and return the DocumentNode, or a GraphQL-shaped error
- * array if the query has a syntax error.
- *
- * @param string $query The GraphQL query string.
- * @return DocumentNode|array
- */
- private function parse( string $query ) {
- try {
- return Parser::parse( $query, array( 'noLocation' => true ) );
- } catch ( \Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError $e ) {
- return $this->error_response( 'GraphQL syntax error: ' . $e->getMessage(), 'GRAPHQL_PARSE_ERROR' );
- }
- }
-
- /**
- * Parse a query, cache the resulting AST, and return the DocumentNode.
- *
- * Writes to OPcache when enabled and usable. APQ registrations always
- * also write to the object cache so hash-only lookups still resolve if
- * OPcache later becomes unavailable (toggle off, dir unwritable, files
- * cleaned up, or a silent write_to_opcache failure).
- *
- * Returns an error array if the query has a syntax error.
- *
- * @param string $query The GraphQL query string.
- * @param string $hash The SHA-256 hash to cache under.
- * @param bool $for_apq Whether the request is an APQ registration.
- * @return DocumentNode|array
- */
- private function parse_and_cache( string $query, string $hash, bool $for_apq = false ) {
- $document = $this->parse( $query );
- if ( ! $document instanceof DocumentNode ) {
- return $document;
- }
-
- $used_opcache = Main::is_opcache_enabled() && $this->is_opcache_usable();
- if ( $used_opcache ) {
- $this->write_to_opcache( $hash, $document );
- }
-
- if ( $for_apq || ( Main::is_object_cache_enabled() && ! $used_opcache ) ) {
- wp_cache_set( $this->build_cache_key( $hash ), $document->toArray(), self::CACHE_GROUP, self::get_cache_ttl() );
- }
-
- return $document;
- }
-
- /**
- * Build a versioned cache key from a hash.
- *
- * @param string $hash The SHA-256 hash.
- * @return string
- */
- private function build_cache_key( string $hash ): string {
- return self::CACHE_KEY_PREFIX . $hash;
- }
-
- /**
- * Whether the OPcache file backend can be used for this request.
- *
- * Memoised per request: the underlying checks (opcache_get_status,
- * filesystem writability) don't change mid-request and are wasteful
- * to repeat across the read and write paths.
- */
- private function is_opcache_usable(): bool {
- if ( null !== $this->opcache_usable ) {
- return $this->opcache_usable;
- }
-
- $this->opcache_usable = $this->compute_is_opcache_usable();
- return $this->opcache_usable;
- }
-
- /**
- * Underlying capability check for the OPcache file backend.
- *
- * Requires the OPcache extension to be loaded and enabled, and the cache
- * directory to exist (or be creatable) and be writable.
- */
- private function compute_is_opcache_usable(): bool {
- if ( ! function_exists( 'opcache_get_status' ) || ! ini_get( 'opcache.enable' ) ) {
- return false;
- }
-
- // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- opcache.restrict_api raises E_WARNING when the calling path is disallowed; the false return is handled below.
- $status = @opcache_get_status( false );
- if ( ! is_array( $status ) || empty( $status['opcache_enabled'] ) ) {
- return false;
- }
-
- return $this->ensure_opcache_dir_writable();
- }
-
- /**
- * Resolve the directory where OPcache cache files are written.
- *
- * Defaults to a versioned subdirectory under wp-uploads so it inherits
- * the writability guarantees WordPress places on uploads. Filterable
- * for tests and unusual hosting layouts.
- *
- * @internal Public for {@see OpcacheFileExpiry}; not part of the plugin's external API.
- */
- public static function get_opcache_cache_dir(): string {
- $upload_dir = wp_get_upload_dir();
- $default = trailingslashit( $upload_dir['basedir'] ) . self::OPCACHE_DIR_RELATIVE;
-
- /**
- * Filters the directory where parsed GraphQL ASTs are written for OPcache.
- *
- * @since 10.9.0
- *
- * @param string $dir Default cache directory under wp-uploads.
- */
- $dir = (string) apply_filters( 'woocommerce_graphql_opcache_cache_dir', $default );
-
- // Reject stream wrappers (e.g. phar://, http://) to keep file_put_contents,
- // rename, and include constrained to local filesystem paths.
- if ( '' === $dir || wp_is_stream( $dir ) ) {
- return '';
- }
-
- return $dir;
- }
-
- /**
- * Ensure the OPcache cache directory exists and is writable.
- *
- * Creates the directory on first use and drops a deny-all .htaccess and
- * an empty index.html alongside it. Returns false if creation fails or
- * the directory ends up non-writable.
- */
- private function ensure_opcache_dir_writable(): bool {
- $dir = self::get_opcache_cache_dir();
-
- if ( '' === $dir ) {
- return false;
- }
-
- if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
- return false;
- }
-
- $fs = ResolverHelpers::wp_filesystem();
- if ( ! $fs || ! $fs->is_writable( $dir ) ) {
- return false;
- }
-
- // Best-effort hardening; ignore failures (e.g. read-only permissions).
- $htaccess = $dir . '/.htaccess';
- $index = $dir . '/index.html';
- if ( ! file_exists( $htaccess ) ) {
- $fs->put_contents( $htaccess, "Deny from all\n" );
- }
- if ( ! file_exists( $index ) ) {
- $fs->put_contents( $index, '' );
- }
-
- return true;
- }
-
- /**
- * Read a cached DocumentNode from the OPcache file backend.
- *
- * @param string $hash The SHA-256 hash.
- * @return DocumentNode|false
- */
- private function read_from_opcache( string $hash ) {
- $path = self::get_opcache_cache_dir() . '/' . $hash . '.php';
-
- if ( ! is_file( $path ) ) {
- return false;
- }
-
- // File contents are produced by self::write_to_opcache() and only
- // ever return a primitive array. The caller falls back to parsing
- // when the include returns a non-array.
- $data = include $path;
-
- if ( ! is_array( $data ) ) {
- return false;
- }
-
- try {
- return AST::fromArray( $data );
- } catch ( \Throwable $e ) {
- return false;
- } finally {
- OpcacheFileExpiry::ensure_scheduled();
- }
- }
-
- /**
- * Persist a parsed AST to the OPcache file backend.
- *
- * Writes atomically (temp file + rename) so concurrent readers never see
- * a partial file, and explicitly invalidates OPcache for the destination
- * path so installs running with opcache.validate_timestamps=0 still see
- * the new version.
- *
- * Failures are intentionally silent: the caller already holds a valid
- * DocumentNode, and a failed cache write only forfeits the optimisation
- * for one request.
- *
- * @param string $hash The SHA-256 hash to cache under.
- * @param DocumentNode $document The parsed AST.
- */
- private function write_to_opcache( string $hash, DocumentNode $document ): void {
- $dir = self::get_opcache_cache_dir();
- $path = $dir . '/' . $hash . '.php';
- $tmp = $path . '.' . bin2hex( random_bytes( 8 ) ) . '.tmp';
-
- $contents = "<?php\nreturn " . var_export( $document->toArray(), true ) . ";\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
-
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
- if ( false === file_put_contents( $tmp, $contents, LOCK_EX ) ) {
- return;
- }
-
- $fs = ResolverHelpers::wp_filesystem();
- if ( ! $fs || ! $fs->move( $tmp, $path, true ) ) {
- if ( $fs ) {
- $fs->delete( $tmp );
- }
- return;
- }
-
- if ( function_exists( 'opcache_invalidate' ) ) {
- opcache_invalidate( $path, true );
- }
- if ( function_exists( 'opcache_compile_file' ) ) {
- // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
- @opcache_compile_file( $path );
- }
-
- OpcacheFileExpiry::ensure_scheduled();
- }
-
- /**
- * Build a GraphQL-shaped error response array.
- *
- * @param string $message The error message.
- * @param string $code The error code for extensions.
- * @return array
- */
- private function error_response( string $message, string $code ): array {
- return array(
- 'errors' => array(
- array(
- 'message' => $message,
- 'extensions' => array( 'code' => $code ),
- ),
- ),
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/QueryComplexityRule.php b/plugins/woocommerce/src/Internal/Api/QueryComplexityRule.php
deleted file mode 100644
index fc260660c67..00000000000
--- a/plugins/woocommerce/src/Internal/Api/QueryComplexityRule.php
+++ /dev/null
@@ -1,283 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
-
-/**
- * QueryComplexity validation rule that returns a generic error message when
- * the complexity is exceeded. Admins can still read both values via debug
- * mode; see {@see GraphQLController} step 8.
- *
- * Unlike the stock webonyx rule, the work done stays proportional to the size
- * of the document: each named fragment is scored once and the result reused
- * for every spread, variable values are coerced once instead of once per
- * directive or complexity callback, field definitions come from the visitor's
- * TypeInfo instead of being re-collected for every selection set, and scores
- * saturate at {@see self::COMPLEXITY_CEILING} instead of overflowing.
- */
-class QueryComplexityRule extends QueryComplexity {
- /**
- * Upper bound for computed complexity scores.
- *
- * Far above any configurable limit, so real scores stay exact, while leaving
- * headroom below PHP_INT_MAX for complexity callbacks to multiply a saturated
- * child score by a page size without overflowing.
- */
- public const COMPLEXITY_CEILING = PHP_INT_MAX >> 10;
-
- /**
- * Memoized complexity of each named fragment, keyed by fragment name.
- *
- * @var array<string, int>
- */
- private array $fragment_complexities = array();
-
- /**
- * Names of the fragments whose complexity is currently being computed;
- * guards against fragment cycles (which the NoFragmentCycles rule reports).
- *
- * @var array<string, true>
- */
- private array $fragments_in_progress = array();
-
- /**
- * Variable values coerced for the current document, or null when not yet computed.
- *
- * @var ?array<string, mixed>
- */
- private ?array $coerced_variable_values = null;
-
- /**
- * Schema definition of every field node in the document, keyed by the
- * node's spl_object_id(). Populated as the visitor enters each field.
- *
- * @var array<int, ?FieldDefinition>
- */
- private array $field_definitions = array();
-
- /**
- * Reset the per-document state, then replace the stock SELECTION_SET
- * callback, which re-collects field definitions through every fragment
- * reachable from each selection set, with recording the definition that
- * TypeInfo already resolves as the visitor enters each field.
- *
- * @param QueryValidationContext $context The validation context.
- * @return array The visitor definition.
- */
- public function getVisitor( QueryValidationContext $context ): array {
- $this->fragment_complexities = array();
- $this->fragments_in_progress = array();
- $this->coerced_variable_values = null;
- $this->field_definitions = array();
-
- $visitor = parent::getVisitor( $context );
- if ( array() === $visitor ) {
- // The rule is disabled.
- return $visitor;
- }
-
- unset( $visitor[ NodeKind::SELECTION_SET ] );
- $visitor[ NodeKind::FIELD ] = function ( FieldNode $node ) use ( $context ): void {
- $this->field_definitions[ spl_object_id( $node ) ] = $context->getFieldDef();
- };
-
- return $visitor;
- }
-
- /**
- * Look up the schema definition recorded for a field node.
- *
- * @param FieldNode $field The field node.
- * @return ?FieldDefinition The definition, or null when the field doesn't exist on its parent type.
- */
- protected function fieldDefinition( FieldNode $field ): ?FieldDefinition {
- return $this->field_definitions[ spl_object_id( $field ) ] ?? null;
- }
-
- /**
- * Sum the complexity of a selection set's selections, saturating at
- * {@see self::COMPLEXITY_CEILING}.
- *
- * @param SelectionSetNode $selection_set The selection set to score.
- * @return int The (possibly saturated) complexity.
- * @throws \Exception When variable or argument coercion fails.
- */
- protected function fieldComplexity( SelectionSetNode $selection_set ): int {
- $complexity = 0;
-
- foreach ( $selection_set->selections as $selection ) {
- $complexity = $this->add_saturating( $complexity, $this->nodeComplexity( $selection ) );
- }
-
- return $complexity;
- }
-
- /**
- * Score a single selection. Named fragments are scored once and the result
- * reused for every spread; everything else is delegated to the stock rule.
- *
- * @param SelectionNode $node The selection to score.
- * @return int The complexity of the selection.
- * @throws \Exception When variable or argument coercion fails.
- */
- protected function nodeComplexity( SelectionNode $node ): int {
- if ( ! $node instanceof FragmentSpreadNode ) {
- return parent::nodeComplexity( $node );
- }
-
- $fragment = $this->getFragment( $node );
- if ( is_null( $fragment ) ) {
- return 0;
- }
-
- $name = $fragment->name->value;
- if ( array_key_exists( $name, $this->fragment_complexities ) ) {
- return $this->fragment_complexities[ $name ];
- }
-
- // A fragment that (transitively) spreads itself has unbounded
- // complexity. NoFragmentCycles reports the actual error.
- if ( isset( $this->fragments_in_progress[ $name ] ) ) {
- return self::COMPLEXITY_CEILING;
- }
-
- $this->fragments_in_progress[ $name ] = true;
- try {
- $complexity = $this->fieldComplexity( $fragment->selectionSet );
- } finally {
- unset( $this->fragments_in_progress[ $name ] );
- }
-
- $this->fragment_complexities[ $name ] = $complexity;
-
- return $complexity;
- }
-
- /**
- * Whether `@include` / `@skip` directives exclude the field from execution.
- *
- * Same semantics as the stock rule, but variable values are coerced once
- * per document (see {@see self::get_coerced_variable_values()}).
- *
- * @param FieldNode $node The field node.
- * @return bool True when the field will not be executed.
- * @throws \Exception When variable coercion fails.
- */
- protected function directiveExcludesField( FieldNode $node ): bool {
- foreach ( $node->directives as $directive_node ) {
- $directive_name = $directive_node->name->value;
-
- if ( Directive::INCLUDE_NAME === $directive_name ) {
- $include_arguments = Values::getArgumentValues(
- Directive::includeDirective(),
- $directive_node,
- $this->get_coerced_variable_values()
- );
- if ( false === $include_arguments['if'] ) {
- return true;
- }
- } elseif ( Directive::SKIP_NAME === $directive_name ) {
- $skip_arguments = Values::getArgumentValues(
- Directive::skipDirective(),
- $directive_node,
- $this->get_coerced_variable_values()
- );
- if ( true === $skip_arguments['if'] ) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Build the argument values handed to a field's complexity callback.
- *
- * Same semantics as the stock rule, but variable values are coerced once
- * per document (see {@see self::get_coerced_variable_values()}).
- *
- * @param FieldNode $node The field node.
- * @return array<string, mixed> The coerced argument values.
- * @throws \Exception When variable or argument coercion fails.
- */
- protected function buildFieldArguments( FieldNode $node ): array {
- $field_definition = $this->fieldDefinition( $node );
-
- return $field_definition instanceof FieldDefinition
- ? Values::getArgumentValues( $field_definition, $node, $this->get_coerced_variable_values() )
- : array();
- }
-
- /**
- * Coerce the document's variable values against their definitions,
- * once per document.
- *
- * @return array<string, mixed> The coerced variable values.
- * @throws Error When the provided variables don't satisfy their definitions (same error the stock rule throws).
- */
- private function get_coerced_variable_values(): array {
- if ( ! is_null( $this->coerced_variable_values ) ) {
- return $this->coerced_variable_values;
- }
-
- list( $errors, $variable_values ) = Values::getVariableValues(
- $this->context->getSchema(),
- $this->variableDefs,
- $this->getRawVariableValues()
- );
-
- if ( ! empty( $errors ) ) {
- // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON by the GraphQL error formatter.
- throw new Error(
- implode(
- "\n\n",
- array_map( static fn( Error $error ): string => $error->getMessage(), $errors )
- )
- );
- // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
- }
-
- $this->coerced_variable_values = $variable_values ?? array();
-
- return $this->coerced_variable_values;
- }
-
- /**
- * Add two complexity scores, saturating at {@see self::COMPLEXITY_CEILING}.
- *
- * @param int $a First score.
- * @param int $b Second score.
- * @return int The saturated sum.
- */
- private function add_saturating( int $a, int $b ): int {
- $sum = $a + $b;
-
- // An int overflow turns the sum into a float, which is also above the ceiling.
- return $sum > self::COMPLEXITY_CEILING ? self::COMPLEXITY_CEILING : (int) $sum;
- }
-
- /**
- * Override webonyx's default ("Max query complexity should be {max} but
- * got {count}.").
- *
- * @param int $max The configured maximum complexity (unused).
- * @param int $count The computed query complexity (unused).
- */
- public static function maxQueryComplexityErrorMessage( int $max, int $count ): string {
- return 'Maximum query complexity exceeded.';
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/QueryDepthRule.php b/plugins/woocommerce/src/Internal/Api/QueryDepthRule.php
deleted file mode 100644
index 1b1d885c6c9..00000000000
--- a/plugins/woocommerce/src/Internal/Api/QueryDepthRule.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryDepth;
-
-/**
- * QueryDepth validation rule that returns a generic error message when the
- * depth is exceeded. Admins can still read both values via debug mode; see
- * {@see GraphQLController} step 8.
- *
- * Unlike the stock webonyx rule, which walks a named fragment again on every
- * spread, each fragment's depth is computed once, relative to the position it
- * is spread at, and reused.
- */
-class QueryDepthRule extends QueryDepth {
- /**
- * Sentinel for a selection tree with no nested selection sets, which adds
- * no depth wherever it is spread. The stock walk only ever raises the
- * running maximum, so seeding it with -1 makes the same walk report either
- * the relative depth (>= 0) or this sentinel.
- */
- private const NO_NESTED_FIELDS = -1;
-
- /**
- * Memoized relative depth of each named fragment, keyed by fragment name.
- *
- * @var array<string, int>
- */
- private array $fragment_depths = array();
-
- /**
- * Reset the per-document memoization before delegating to the stock visitor.
- *
- * @param QueryValidationContext $context The validation context.
- * @return array The visitor definition.
- */
- public function getVisitor( QueryValidationContext $context ): array {
- $this->fragment_depths = array();
-
- return parent::getVisitor( $context );
- }
-
- /**
- * Compute the depth reached below a selection. Named fragment spreads use
- * the fragment's relative depth, computed once; everything else is
- * delegated to the stock rule.
- *
- * @param Node $node The selection node.
- * @param int $depth The depth the selection sits at.
- * @param int $max_depth The maximum depth seen so far.
- * @return int The updated maximum depth.
- */
- protected function nodeDepth( Node $node, int $depth = 0, int $max_depth = 0 ): int {
- if ( ! $node instanceof FragmentSpreadNode ) {
- return parent::nodeDepth( $node, $depth, $max_depth );
- }
-
- $fragment = $this->getFragment( $node );
- if ( is_null( $fragment ) ) {
- return $max_depth;
- }
-
- $name = $fragment->name->value;
- if ( ! array_key_exists( $name, $this->fragment_depths ) ) {
- // Same cycle guard as the stock rule: a fragment that (transitively)
- // spreads itself is reported as exceeding the limit.
- if ( isset( $this->calculatedFragments[ $name ] ) ) {
- return $this->maxQueryDepth + 1;
- }
-
- $this->calculatedFragments[ $name ] = true;
- try {
- $this->fragment_depths[ $name ] = $this->fieldDepth( $fragment, 0, self::NO_NESTED_FIELDS );
- } finally {
- unset( $this->calculatedFragments[ $name ] );
- }
- }
-
- $relative_depth = $this->fragment_depths[ $name ];
-
- return self::NO_NESTED_FIELDS === $relative_depth
- ? $max_depth
- : max( $max_depth, $depth + $relative_depth );
- }
-
- /**
- * Override webonyx's default ("Max query depth should be {max} but
- * got {count}.").
- *
- * @param int $max The configured maximum depth (unused).
- * @param int $count The computed query depth (unused).
- */
- public static function maxQueryDepthErrorMessage( int $max, int $count ): string {
- return 'Maximum query depth exceeded.';
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/README.md b/plugins/woocommerce/src/Internal/Api/README.md
deleted file mode 100644
index a84fa0e588a..00000000000
--- a/plugins/woocommerce/src/Internal/Api/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Important: Internal and experimental code
-
-**ALL** the code that's inside the `Automattic\WooCommerce\Internal` namespace and nested namespaces, or that's annotated with `@internal`, is for exclusive usage of WooCommerce core and must **NEVER** be used in released extensions or otherwise in production environments.
-
-Additionally, the code in this directory (`Automattic\WooCommerce\Internal\Api` namespace and nested namespaces) is part of [an experimental feature](https://github.com/woocommerce/woocommerce/pull/63772) that could get backwards-incompatible changes or even be completely removed in future versions of WooCommerce; moreover, it's infrastructure code that's really not intended for external usage.
-
-If you want to experiment with the feature (**NEVER** in production environments) from the code side, read [the provisional documentation](https://github.com/woocommerce/woocommerce/pull/63772) and look at the classes in the `src/Api` namespace.
diff --git a/plugins/woocommerce/src/Internal/Api/Settings.php b/plugins/woocommerce/src/Internal/Api/Settings.php
deleted file mode 100644
index aa7bb8594de..00000000000
--- a/plugins/woocommerce/src/Internal/Api/Settings.php
+++ /dev/null
@@ -1,187 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-
-/**
- * Settings handling for the GraphQL API.
- *
- * Registers the "GraphQL" section under WooCommerce - Settings - Advanced.
- * Only active when Main::is_enabled() returns true (feature flag on and
- * PHP 8.1+), so the section is hidden when the feature is disabled.
- */
-class Settings {
- /**
- * Identifier for the GraphQL section under the Advanced settings tab.
- */
- public const SECTION_ID = 'graphql';
-
- /**
- * Register the filter hooks that expose the GraphQL settings section.
- */
- public function register(): void {
- add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_section' ) );
- add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_settings' ), 10, 2 );
- add_filter(
- 'woocommerce_admin_settings_sanitize_option_' . Main::OPTION_ENDPOINT_URL,
- array( $this, 'sanitize_endpoint_url' ),
- 10,
- 3
- );
- }
-
- /**
- * Append the GraphQL section to the Advanced settings tab.
- *
- * @param array $sections Existing sections keyed by id.
- * @return array
- */
- public function add_section( array $sections ): array {
- if ( Main::is_enabled() ) {
- $sections[ self::SECTION_ID ] = __( 'GraphQL', 'woocommerce' );
- }
- return $sections;
- }
-
- /**
- * Provide the settings fields for the GraphQL section.
- *
- * @param array $settings Existing settings for the current section.
- * @param string $section_id Current section id.
- * @return array
- */
- public function add_settings( array $settings, string $section_id ): array {
- if ( self::SECTION_ID !== $section_id || ! Main::is_enabled() ) {
- return $settings;
- }
-
- return array(
- array(
- 'title' => __( 'GraphQL', 'woocommerce' ),
- 'desc' => __( 'Configure the WooCommerce GraphQL API.', 'woocommerce' ),
- 'type' => 'title',
- 'id' => 'woocommerce_graphql_options',
- ),
- array(
- 'title' => __( 'Endpoint URL', 'woocommerce' ),
- 'desc' => __( 'Path relative to /wp-json/ where the GraphQL endpoint is exposed. Needs at least two segments (namespace/route), e.g. wc/graphql.', 'woocommerce' ),
- 'desc_tip' => true,
- 'id' => Main::OPTION_ENDPOINT_URL,
- 'default' => GraphQLControllerBase::DEFAULT_ENDPOINT_URL,
- 'type' => 'text',
- ),
- array(
- 'title' => __( 'Enable GET endpoint', 'woocommerce' ),
- 'desc' => __( 'Allow GraphQL queries over GET in addition to POST', 'woocommerce' ),
- 'id' => Main::OPTION_GET_ENDPOINT_ENABLED,
- 'default' => 'yes',
- 'type' => 'checkbox',
- ),
- array(
- 'title' => __( 'Maximum query depth', 'woocommerce' ),
- 'desc' => __( 'Reject queries whose selection nesting exceeds this depth.', 'woocommerce' ),
- 'id' => Main::OPTION_MAX_QUERY_DEPTH,
- 'default' => (string) GraphQLControllerBase::DEFAULT_MAX_QUERY_DEPTH,
- 'type' => 'number',
- 'custom_attributes' => array( 'min' => '1' ),
- ),
- array(
- 'title' => __( 'Maximum query complexity', 'woocommerce' ),
- 'desc' => __( 'Reject queries whose computed complexity score exceeds this value.', 'woocommerce' ),
- 'id' => Main::OPTION_MAX_QUERY_COMPLEXITY,
- 'default' => (string) GraphQLControllerBase::DEFAULT_MAX_QUERY_COMPLEXITY,
- 'type' => 'number',
- 'custom_attributes' => array( 'min' => '1' ),
- ),
- array(
- 'title' => __( 'Enable OPcache-based caching', 'woocommerce' ),
- 'desc' => __( 'Cache parsed queries on disk as PHP files so OPcache can serve them from shared memory. Falls back to the object cache when the filesystem is not writable.', 'woocommerce' ),
- 'id' => Main::OPTION_OPCACHE_ENABLED,
- 'default' => 'yes',
- 'type' => 'checkbox',
- ),
- array(
- 'title' => __( 'Enable ObjectCache-based caching', 'woocommerce' ),
- 'desc' => __( 'Cache parsed queries in the WP object cache', 'woocommerce' ),
- 'id' => Main::OPTION_OBJECT_CACHE_ENABLED,
- 'default' => 'yes',
- 'type' => 'checkbox',
- ),
- array(
- 'title' => __( 'Enable APQ caching', 'woocommerce' ),
- 'desc' => __( 'Cache parsed queries using the Apollo Automatic Persisted Queries protocol', 'woocommerce' ),
- 'id' => Main::OPTION_APQ_ENABLED,
- 'default' => 'yes',
- 'type' => 'checkbox',
- ),
- array(
- 'title' => __( 'Parsed query cache TTL', 'woocommerce' ),
- 'desc' => __( 'Time in seconds before cached parsed queries expire.', 'woocommerce' ),
- 'id' => Main::OPTION_QUERY_CACHE_TTL,
- 'default' => (string) QueryCache::DEFAULT_CACHE_TTL,
- 'type' => 'number',
- 'custom_attributes' => array( 'min' => '1' ),
- ),
- array(
- 'type' => 'sectionend',
- 'id' => 'woocommerce_graphql_options',
- ),
- );
- }
-
- /**
- * Validate and normalize the endpoint URL on save.
- *
- * Rejects empty input and inputs without at least two path segments, since
- * register_rest_route() needs both a namespace and a route. Rejects any
- * character outside of what WordPress REST routes accept (alphanumerics,
- * underscores, hyphens). On rejection, adds a settings error message and
- * returns the previously stored value so the option is not overwritten.
- *
- * @param mixed $value The sanitized value passed by earlier filters.
- * @param array $option The option config from add_settings().
- * @param mixed $raw_value The raw value submitted by the form. Typed as mixed because POST data can be null or an array (e.g. when the field name is submitted as `name[]`).
- * @return string
- */
- public function sanitize_endpoint_url( $value, array $option, $raw_value ): string {
- unset( $value, $option );
-
- $fallback = (string) get_option( Main::OPTION_ENDPOINT_URL, GraphQLControllerBase::DEFAULT_ENDPOINT_URL );
-
- if ( ! is_string( $raw_value ) ) {
- return $fallback;
- }
-
- $normalized = trim( $raw_value, '/' );
-
- if ( '' === $normalized ) {
- \WC_Admin_Settings::add_error( __( 'GraphQL endpoint URL cannot be empty.', 'woocommerce' ) );
- return $fallback;
- }
-
- $parts = explode( '/', $normalized );
- if ( count( $parts ) < 2 ) {
- \WC_Admin_Settings::add_error( __( 'GraphQL endpoint URL needs at least two segments, e.g. wc/graphql.', 'woocommerce' ) );
- return $fallback;
- }
-
- foreach ( $parts as $part ) {
- if ( '' === $part || ! preg_match( GraphQLControllerBase::ENDPOINT_URL_SEGMENT_PATTERN, $part ) ) {
- \WC_Admin_Settings::add_error(
- sprintf(
- /* translators: %s: the invalid path segment */
- __( 'GraphQL endpoint URL segment "%s" contains invalid characters. Use letters, digits, underscores, and hyphens only.', 'woocommerce' ),
- $part
- )
- );
- return $fallback;
- }
- }
-
- return $normalized;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Api/StatusResolverFailedException.php b/plugins/woocommerce/src/Internal/Api/StatusResolverFailedException.php
deleted file mode 100644
index 19c322d122c..00000000000
--- a/plugins/woocommerce/src/Internal/Api/StatusResolverFailedException.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Api;
-
-/**
- * Internal sentinel raised by {@see GraphQLController::pick_status()} when a
- * plugin-supplied HTTP status resolver throws.
- *
- * The resolver is documented as "must not throw"; this exception lets the
- * controller distinguish a resolver bug from any other Throwable so it can
- * short-circuit to a fixed-shape 500 response without re-invoking the
- * resolver. Never surfaced on the wire.
- *
- * @internal
- */
-final class StatusResolverFailedException extends \RuntimeException {
-}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 7ef2bd15c22..63320e87469 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -610,18 +610,6 @@ class FeaturesController {
'disable_ui' => false,
'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
),
- 'dual_code_graphql_api' => array(
- 'name' => __( 'Dual Code & GraphQL API', 'woocommerce' ),
- 'description' => __(
- 'Experimental code-first API for WooCommerce with automatic GraphQL endpoint generation. Requires PHP 8.1 or later.',
- 'woocommerce'
- ),
- 'enabled_by_default' => false,
- 'is_experimental' => true,
- 'disable_ui' => true,
- 'skip_compatibility_checks' => true,
- 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
- ),
PushNotifications::FEATURE_NAME => array(
'name' => __( 'Push Notifications', 'woocommerce' ),
'description' => __(
diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php
index 61ad96a6d3d..d78ee26040e 100644
--- a/plugins/woocommerce/tests/legacy/bootstrap.php
+++ b/plugins/woocommerce/tests/legacy/bootstrap.php
@@ -92,8 +92,6 @@ class WC_Unit_Tests_Bootstrap {
// load the WP testing environment.
require_once $this->wp_tests_dir . '/includes/bootstrap.php';
- $this->maybe_announce_skipped_graphql_tests();
-
// Ensure theme install tests use direct filesystem method.
if ( ! defined( 'FS_METHOD' ) ) {
define( 'FS_METHOD', 'direct' );
@@ -199,45 +197,6 @@ class WC_Unit_Tests_Bootstrap {
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::toggle_cot_feature_and_usage( ! $disable_hpos );
}
- /**
- * Echo a "Not running GraphQL …" message when an explicit `--testsuite`
- * filter is given that omits `wc-phpunit-graphql`, mirroring the "Not
- * running ajax tests" line printed by WP's own bootstrap for the `ajax`,
- * `ms-files` and `external-http` groups.
- *
- * The GraphQL suite is kept separate because it requires PHP 8.1+, so
- * PHP 7.4 / 8.0 CI jobs point `--testsuite` at the legacy + main suites
- * only. A default run (no `--testsuite` filter) runs the full suite list,
- * which includes the GraphQL suite, so there is nothing to announce. The
- * `--testsuite` value may be a comma-joined suite list, hence the substring
- * match rather than an exact comparison.
- */
- private function maybe_announce_skipped_graphql_tests() {
- $argv = isset( $GLOBALS['argv'] ) && is_array( $GLOBALS['argv'] ) ? $GLOBALS['argv'] : array();
-
- $has_testsuite_filter = false;
- $running_graphql = false;
- foreach ( $argv as $arg ) {
- if ( ! is_string( $arg ) ) {
- continue;
- }
- if ( false !== strpos( $arg, '--testsuite' ) ) {
- $has_testsuite_filter = true;
- }
- if ( false !== strpos( $arg, 'wc-phpunit-graphql' ) ) {
- $running_graphql = true;
- }
- }
-
- // Without an explicit --testsuite filter the default suite list runs,
- // which already includes the GraphQL suite: nothing is skipped.
- if ( ! $has_testsuite_filter || $running_graphql ) {
- return;
- }
-
- echo 'Not running GraphQL tests. To execute these, add wc-phpunit-graphql to --testsuite (a default run without --testsuite includes it).' . PHP_EOL;
- }
-
/**
* Re-initialize the dependency injection engine.
*
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/DesignTime/StalenessCheckerTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/DesignTime/StalenessCheckerTest.php
deleted file mode 100644
index f1ce3306025..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/DesignTime/StalenessCheckerTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure\DesignTime;
-
-use Automattic\WooCommerce\Api\Infrastructure\DesignTime\StalenessChecker;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see StalenessChecker} — the hash-based staleness detector used
- * by `pnpm run build:api:check` to decide whether the autogenerated GraphQL
- * API code matches the current code-API sources.
- */
-class StalenessCheckerTest extends WC_Unit_Test_Case {
- private string $api_dir;
- private string $autogen_dir;
-
- public function setUp(): void {
- parent::setUp();
- $base = sys_get_temp_dir() . '/staleness-checker-' . uniqid( '', true );
- $this->api_dir = $base . '/Api';
- $this->autogen_dir = $base . '/Autogenerated';
- mkdir( $this->api_dir, 0777, true );
- mkdir( $this->autogen_dir, 0777, true );
- file_put_contents( $this->api_dir . '/Widget.php', "<?php\nclass Widget {}\n" );
- mkdir( $this->api_dir . '/Sub' );
- file_put_contents( $this->api_dir . '/Sub/Nested.php', "<?php\nclass Nested {}\n" );
- }
-
- public function tearDown(): void {
- $this->rmdir_recursive( dirname( $this->api_dir ) );
- parent::tearDown();
- }
-
- public function test_is_stale_returns_true_when_hash_file_is_missing(): void {
- $this->assertTrue( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_is_stale_returns_false_when_stored_hash_matches_current_sources(): void {
- $this->write_hash_file( StalenessChecker::compute_source_hash( $this->api_dir ) );
- $this->assertFalse( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_is_stale_returns_true_when_a_source_file_was_modified_after_the_hash_was_recorded(): void {
- $this->write_hash_file( StalenessChecker::compute_source_hash( $this->api_dir ) );
- file_put_contents( $this->api_dir . '/Widget.php', "<?php\nclass Widget { public int \$x = 1; }\n" );
- $this->assertTrue( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_is_stale_returns_true_when_a_source_file_was_renamed(): void {
- $this->write_hash_file( StalenessChecker::compute_source_hash( $this->api_dir ) );
- rename( $this->api_dir . '/Widget.php', $this->api_dir . '/Gadget.php' );
- $this->assertTrue( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_is_stale_returns_true_when_a_source_file_was_added(): void {
- $this->write_hash_file( StalenessChecker::compute_source_hash( $this->api_dir ) );
- file_put_contents( $this->api_dir . '/NewClass.php', "<?php\nclass NewClass {}\n" );
- $this->assertTrue( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_is_stale_ignores_non_php_files(): void {
- $this->write_hash_file( StalenessChecker::compute_source_hash( $this->api_dir ) );
- file_put_contents( $this->api_dir . '/README.md', 'docs' );
- $this->assertFalse( StalenessChecker::is_stale( $this->api_dir, $this->autogen_dir ) );
- }
-
- public function test_compute_source_hash_is_stable_across_runs(): void {
- $first = StalenessChecker::compute_source_hash( $this->api_dir );
- $second = StalenessChecker::compute_source_hash( $this->api_dir );
- $this->assertSame( $first, $second );
- }
-
- private function write_hash_file( string $hash ): void {
- file_put_contents( $this->autogen_dir . '/api_source_hash.txt', $hash );
- }
-
- private function rmdir_recursive( string $dir ): void {
- if ( ! is_dir( $dir ) ) {
- return;
- }
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ),
- \RecursiveIteratorIterator::CHILD_FIRST
- );
- foreach ( $iterator as $file ) {
- $file->isDir() ? rmdir( $file->getPathname() ) : unlink( $file->getPathname() );
- }
- rmdir( $dir );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/QueryInfoExtractorTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/QueryInfoExtractorTest.php
deleted file mode 100644
index 6a3b686225a..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/QueryInfoExtractorTest.php
+++ /dev/null
@@ -1,323 +0,0 @@
-<?php
-/**
- * QueryInfoExtractor tests — interact with webonyx AST nodes whose properties
- * (selectionSet, fieldNodes, variableValues, …) are camelCase by design.
- *
- * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
- */
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\CountingNodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see QueryInfoExtractor}. These exercise the AST → query-info
- * tree transformation that mappers consume to skip work for unselected fields.
- */
-class QueryInfoExtractorTest extends WC_Unit_Test_Case {
- /**
- * Parse a GraphQL operation and return the top-level FieldNode plus the
- * fragment-definition map that ResolveInfo would expose.
- *
- * @param string $source A GraphQL document containing one query.
- * @return array{0: FieldNode, 1: array<string, FragmentDefinitionNode>}
- */
- private function parse_top_field( string $source ): array {
- /** @var DocumentNode $doc */
- $doc = Parser::parse( $source, array( 'noLocation' => true ) );
- $operation = null;
- $fragments = array();
- foreach ( $doc->definitions as $def ) {
- if ( $def instanceof OperationDefinitionNode ) {
- $operation = $def;
- } elseif ( $def instanceof FragmentDefinitionNode ) {
- $fragments[ $def->name->value ] = $def;
- }
- }
- $this->assertNotNull( $operation );
- $selections = iterator_to_array( $operation->selectionSet->selections );
- $top_field = $selections[0];
- $this->assertInstanceOf( FieldNode::class, $top_field );
- return array( $top_field, $fragments );
- }
-
- /**
- * @testdox extract returns true for leaf fields with no args or sub-selections.
- */
- public function test_extract_marks_leaf_fields_as_true(): void {
- [ $field ] = $this->parse_top_field( '{ widget(id: 1) { id name } }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array() );
-
- $this->assertSame( true, $tree['id'] ?? null );
- $this->assertSame( true, $tree['name'] ?? null );
- }
-
- /**
- * @testdox extract recurses into sub-selections.
- */
- public function test_extract_recurses_into_sub_selections(): void {
- [ $field ] = $this->parse_top_field( '{ widget { reviews { nodes { id body } } } }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array() );
-
- $this->assertIsArray( $tree['reviews'] );
- $this->assertIsArray( $tree['reviews']['nodes'] );
- $this->assertSame( true, $tree['reviews']['nodes']['id'] ?? null );
- $this->assertSame( true, $tree['reviews']['nodes']['body'] ?? null );
- }
-
- /**
- * @testdox extract captures field arguments under __args.
- */
- public function test_extract_captures_field_arguments(): void {
- [ $field ] = $this->parse_top_field( '{ root { reviews(first: 5, search: "abc") { nodes { id } } } }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array() );
-
- $this->assertArrayHasKey( '__args', $tree['reviews'] );
- $this->assertSame( 5, $tree['reviews']['__args']['first'] ?? null );
- $this->assertSame( 'abc', $tree['reviews']['__args']['search'] ?? null );
- }
-
- /**
- * @testdox extract resolves variable references in arguments.
- */
- public function test_extract_resolves_variables(): void {
- [ $field ] = $this->parse_top_field( 'query Q($n: Int) { root { reviews(first: $n) { nodes { id } } } }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array( 'n' => 42 ) );
-
- $this->assertSame( 42, $tree['reviews']['__args']['first'] ?? null );
- }
-
- /**
- * @testdox extract represents inline fragments under "...TypeName" keys.
- */
- public function test_extract_emits_inline_fragments_with_typename_prefix(): void {
- [ $field ] = $this->parse_top_field(
- '{ thing { id ... on Widget { color } ... on Gadget { parts_count } } }'
- );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array() );
-
- $this->assertSame( true, $tree['id'] ?? null );
- $this->assertArrayHasKey( '...Widget', $tree );
- $this->assertSame( true, $tree['...Widget']['color'] ?? null );
- $this->assertArrayHasKey( '...Gadget', $tree );
- $this->assertSame( true, $tree['...Gadget']['parts_count'] ?? null );
- }
-
- /**
- * @testdox extract merges inline fragments without a type condition into the parent.
- */
- public function test_extract_merges_inline_fragments_without_type_condition(): void {
- [ $field ] = $this->parse_top_field(
- '{ thing { id ... { name } ... @include(if: true) { sku reviews { nodes { id } } } ... on Widget { color } } }'
- );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array() );
-
- $this->assertSame( true, $tree['id'] ?? null );
- $this->assertSame( true, $tree['name'] ?? null );
- $this->assertSame( true, $tree['sku'] ?? null );
- $this->assertSame( true, $tree['reviews']['nodes']['id'] ?? null );
- $this->assertSame( true, $tree['...Widget']['color'] ?? null );
- $this->assertArrayNotHasKey( '...', $tree );
- }
-
- /**
- * @testdox extract expands named fragment spreads inline into the parent.
- */
- public function test_extract_inlines_named_fragment_spreads(): void {
- [ $field, $fragments ] = $this->parse_top_field(
- 'query Q { thing { ...Core } } fragment Core on Widget { id name }'
- );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- $this->assertSame( true, $tree['id'] ?? null );
- $this->assertSame( true, $tree['name'] ?? null );
- }
-
- /**
- * @testdox extract merges overlapping selections from a fragment spread without dropping detail.
- */
- public function test_extract_merges_overlapping_fragment_selections(): void {
- [ $field, $fragments ] = $this->parse_top_field(
- 'query Q { thing { reviews { nodes { id } } ...AlsoReviews } } '
- . 'fragment AlsoReviews on Widget { reviews { nodes { body } } }'
- );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- $this->assertIsArray( $tree['reviews']['nodes'] ?? null );
- $this->assertSame( true, $tree['reviews']['nodes']['id'] ?? null );
- $this->assertSame( true, $tree['reviews']['nodes']['body'] ?? null );
- }
-
- /**
- * @testdox extract returns an empty array for null selection sets.
- */
- public function test_extract_handles_null_selection_set(): void {
- $this->assertSame( array(), QueryInfoExtractor::extract( null, array() ) );
- }
-
- /**
- * @testdox extract_from_info attaches __args from the top-level args.
- */
- public function test_extract_from_info_includes_top_level_args(): void {
- [ $field ] = $this->parse_top_field( '{ widget(id: 7) { name } }' );
-
- // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- webonyx ResolveInfo properties.
- $info = $this->createMock( ResolveInfo::class );
- $info->fieldNodes = new \ArrayObject( array( $field ) );
- $info->variableValues = array();
- $info->fragments = array();
- // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
-
- $tree = QueryInfoExtractor::extract_from_info( $info, array( 'id' => 7 ) );
-
- $this->assertSame( 7, $tree['__args']['id'] ?? null );
- $this->assertSame( true, $tree['name'] ?? null );
- }
-
- /**
- * @testdox extract_from_info skips __args when the args array is empty.
- */
- public function test_extract_from_info_omits_args_when_empty(): void {
- [ $field ] = $this->parse_top_field( '{ widget { name } }' );
-
- // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- webonyx ResolveInfo properties.
- $info = $this->createMock( ResolveInfo::class );
- $info->fieldNodes = new \ArrayObject( array( $field ) );
- $info->variableValues = array();
- $info->fragments = array();
- // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
-
- $tree = QueryInfoExtractor::extract_from_info( $info, array() );
-
- $this->assertArrayNotHasKey( '__args', $tree );
- }
-
- // The enable above closes the ResolveInfo block only; restore the file-level suppression for the AST properties used below.
- // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
-
- /**
- * @testdox extract expands each named fragment once, so a document whose fragments spread each other twice is processed in linear work.
- */
- public function test_extract_expands_duplicate_fragment_spreads_once_per_fragment(): void {
- // Each fragment spreads the next one twice.
- $fragment_count = 40;
- $source = "{ product { ...F0 } }\n";
- for ( $i = 0; $i < $fragment_count - 1; $i++ ) {
- $next = $i + 1;
- $source .= "fragment F{$i} on Product { ...F{$next} ...F{$next} }\n";
- }
- $source .= 'fragment F' . ( $fragment_count - 1 ) . " on Product { id name }\n";
-
- [ $field, $fragments ] = $this->parse_top_field( $source );
- foreach ( $fragments as $fragment ) {
- CountingNodeList::instrument( $fragment->selectionSet );
- }
- CountingNodeList::reset();
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- // Each fragment's selections are iterated exactly once, rather than once per spread.
- $this->assertSame( $fragment_count, CountingNodeList::$iterations );
- $this->assertSame(
- array(
- 'id' => true,
- 'name' => true,
- ),
- $tree
- );
- }
-
- /**
- * @testdox extract yields the same tree for repeated spreads of a memoized fragment as for a single spread.
- */
- public function test_extract_repeated_spreads_of_the_same_fragment_are_idempotent(): void {
- [ $field, $fragments ] = $this->parse_top_field(
- '{ root { product { id ...Details } ...Extra ...Extra } } '
- . 'fragment Details on Product { name price { amount } } '
- . 'fragment Extra on Root { product { price { currency } } other(id: 3) { id } }'
- );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- $this->assertSame(
- array(
- 'product' => array(
- 'id' => true,
- 'name' => true,
- 'price' => array(
- 'amount' => true,
- 'currency' => true,
- ),
- ),
- 'other' => array(
- '__args' => array( 'id' => 3 ),
- 'id' => true,
- ),
- ),
- $tree
- );
- }
-
- /**
- * @testdox extract expands the same fragment under every parent it is spread in.
- */
- public function test_extract_expands_the_same_fragment_under_different_parents(): void {
- [ $field, $fragments ] = $this->parse_top_field( '{ root { a { ...F } b { ...F } } } fragment F on Thing { x y }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- $this->assertSame(
- array(
- 'a' => array(
- 'x' => true,
- 'y' => true,
- ),
- 'b' => array(
- 'x' => true,
- 'y' => true,
- ),
- ),
- $tree
- );
- }
-
- /**
- * @testdox extract terminates on a fragment cycle instead of recursing forever.
- */
- public function test_extract_terminates_on_fragment_cycles(): void {
- [ $field, $fragments ] = $this->parse_top_field( '{ root { ...A } } fragment A on Root { a ...B } fragment B on Root { b ...A }' );
-
- $tree = QueryInfoExtractor::extract( $field->selectionSet, array(), $fragments );
-
- $this->assertArrayHasKey( 'a', $tree );
- $this->assertArrayHasKey( 'b', $tree );
- }
-
- /**
- * @testdox extract ignores spreads of undefined fragments.
- */
- public function test_extract_ignores_undefined_fragments(): void {
- [ $field ] = $this->parse_top_field( '{ root { a ...Missing } }' );
-
- $this->assertSame( array( 'a' => true ), QueryInfoExtractor::extract( $field->selectionSet, array() ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/ResolverHelpersTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/ResolverHelpersTest.php
deleted file mode 100644
index 8812c7b02b6..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/ResolverHelpersTest.php
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error as GraphQLError;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see ResolverHelpers} — the shared helper layer that autogenerated
- * resolvers route pagination construction and exception translation through.
- */
-class ResolverHelpersTest extends WC_Unit_Test_Case {
- /**
- * @testdox complexity_from_pagination scales the cost by the requested page size.
- */
- public function test_complexity_from_pagination_uses_first_when_provided(): void {
- // child_complexity 5, first 10 → 10 * (5 + 1) = 60.
- $this->assertSame( 60, ResolverHelpers::complexity_from_pagination( 5, array( 'first' => 10 ) ) );
- }
-
- /**
- * @testdox complexity_from_pagination falls back to MAX_PAGE_SIZE for invalid values.
- */
- public function test_complexity_from_pagination_clamps_invalid_values(): void {
- $max = PaginationParams::MAX_PAGE_SIZE;
-
- $this->assertSame( $max * ( 1 + 1 ), ResolverHelpers::complexity_from_pagination( 1, array( 'first' => -5 ) ) );
- $this->assertSame( $max * ( 1 + 1 ), ResolverHelpers::complexity_from_pagination( 1, array( 'first' => $max + 1 ) ) );
- $this->assertSame( $max * ( 1 + 1 ), ResolverHelpers::complexity_from_pagination( 1, array( 'first' => 'not-an-int' ) ) );
- }
-
- /**
- * @testdox complexity_from_pagination uses the default page size when no limit is supplied.
- */
- public function test_complexity_from_pagination_uses_default_when_omitted(): void {
- $default = PaginationParams::get_default_page_size();
- $this->assertSame( $default * ( 2 + 1 ), ResolverHelpers::complexity_from_pagination( 2, array() ) );
- }
-
- /**
- * @testdox create_pagination_params builds a PaginationParams from raw args.
- */
- public function test_create_pagination_params_builds_a_pagination_params(): void {
- $params = ResolverHelpers::create_pagination_params(
- array(
- 'first' => 5,
- 'after' => 'cursor-a',
- 'before' => null,
- )
- );
-
- $this->assertInstanceOf( PaginationParams::class, $params );
- $this->assertSame( 5, $params->first );
- $this->assertNull( $params->last );
- $this->assertSame( 'cursor-a', $params->after );
- }
-
- /**
- * @testdox create_pagination_params translates an InvalidArgumentException into INVALID_ARGUMENT.
- */
- public function test_create_pagination_params_translates_invalid_arguments(): void {
- $this->expectException( GraphQLError::class );
- try {
- ResolverHelpers::create_pagination_params( array( 'first' => -1 ) );
- } catch ( GraphQLError $e ) {
- $this->assertSame( 'INVALID_ARGUMENT', $e->getExtensions()['code'] ?? null );
- throw $e;
- }
- }
-
- /**
- * @testdox execute_command forwards return values from the command.
- */
- public function test_execute_command_returns_command_result(): void {
- $command = new class() {
- /**
- * Adds two numbers.
- *
- * @param int $a First operand.
- * @param int $b Second operand.
- */
- public function execute( int $a, int $b ): int {
- return $a + $b;
- }
- };
-
- $result = ResolverHelpers::execute_command(
- $command,
- array(
- 'a' => 2,
- 'b' => 5,
- )
- );
- $this->assertSame( 7, $result );
- }
-
- /**
- * @testdox execute_command translates ApiException into a coded GraphQL error.
- */
- public function test_execute_command_translates_api_exception(): void {
- $command = new class() {
- /**
- * Always throws.
- */
- public function execute(): void {
- throw new ApiException( 'Coupon not found.', 'NOT_FOUND', array( 'detail' => 'extra' ), 404 );
- }
- };
-
- try {
- ResolverHelpers::execute_command( $command, array() );
- $this->fail( 'Expected GraphQLError to be thrown.' );
- } catch ( GraphQLError $e ) {
- $this->assertSame( 'Coupon not found.', $e->getMessage() );
- $extensions = $e->getExtensions();
- $this->assertSame( 'NOT_FOUND', $extensions['code'] ?? null );
- $this->assertSame( 'extra', $extensions['detail'] ?? null );
- }
- }
-
- /**
- * @testdox execute_command preserves the canonical code over a colliding extensions entry.
- */
- public function test_execute_command_canonical_code_wins_over_extensions_entry(): void {
- $command = new class() {
- /**
- * Always throws.
- */
- public function execute(): void {
- throw new ApiException( 'Sneaky', 'NOT_FOUND', array( 'code' => 'OVERRIDDEN' ), 404 );
- }
- };
-
- try {
- ResolverHelpers::execute_command( $command, array() );
- $this->fail( 'Expected GraphQLError to be thrown.' );
- } catch ( GraphQLError $e ) {
- $this->assertSame( 'NOT_FOUND', $e->getExtensions()['code'] ?? null );
- }
- }
-
- /**
- * @testdox execute_command translates InvalidArgumentException to INVALID_ARGUMENT.
- */
- public function test_execute_command_translates_invalid_argument(): void {
- $command = new class() {
- /**
- * Always throws.
- */
- public function execute(): void {
- throw new \InvalidArgumentException( 'bad input' );
- }
- };
-
- try {
- ResolverHelpers::execute_command( $command, array() );
- $this->fail( 'Expected GraphQLError to be thrown.' );
- } catch ( GraphQLError $e ) {
- $this->assertSame( 'bad input', $e->getMessage() );
- $this->assertSame( 'INVALID_ARGUMENT', $e->getExtensions()['code'] ?? null );
- }
- }
-
- /**
- * @testdox execute_command masks unknown throwables behind INTERNAL_ERROR.
- */
- public function test_execute_command_masks_other_throwables(): void {
- $command = new class() {
- /**
- * Always throws.
- */
- public function execute(): void {
- throw new \RuntimeException( 'leaky internals' );
- }
- };
-
- try {
- ResolverHelpers::execute_command( $command, array() );
- $this->fail( 'Expected GraphQLError to be thrown.' );
- } catch ( GraphQLError $e ) {
- $this->assertSame( 'An unexpected error occurred.', $e->getMessage() );
- $this->assertSame( 'INTERNAL_ERROR', $e->getExtensions()['code'] ?? null );
- $this->assertInstanceOf( \RuntimeException::class, $e->getPrevious() );
- }
- }
-
- /**
- * @testdox authorize_command forwards the boolean result.
- */
- public function test_authorize_command_forwards_result(): void {
- $command = new class() {
- /**
- * Echoes the supplied flag.
- *
- * @param bool $allow Whether to allow.
- */
- public function authorize( bool $allow ): bool {
- return $allow;
- }
- };
-
- $this->assertTrue( ResolverHelpers::authorize_command( $command, array( 'allow' => true ) ) );
- $this->assertFalse( ResolverHelpers::authorize_command( $command, array( 'allow' => false ) ) );
- }
-
- /**
- * @testdox translate_exceptions returns the callable result on success.
- */
- public function test_translate_exceptions_returns_callable_result(): void {
- $this->assertSame( 'ok', ResolverHelpers::translate_exceptions( static fn() => 'ok' ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/AliasesTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/AliasesTest.php
deleted file mode 100644
index 5505b311416..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/AliasesTest.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure\Schema;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\AST\StringValueNode as AliasedStringValueNode;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo as AliasedResolveInfo;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
-use WC_Unit_Test_Case;
-
-/**
- * Tests that the `aliases.php` bootstrap registers the surface aliases.
- *
- * These aliases let generated code reference the engine via the
- * Api\Infrastructure\Schema namespace even though webonyx itself constructs
- * the instances. If the alias is broken, every resolver's `resolve()`
- * parameter type-hint check fails at request time.
- */
-class AliasesTest extends WC_Unit_Test_Case {
- /**
- * @testdox the ResolveInfo alias resolves to the webonyx ResolveInfo class.
- */
- public function test_resolve_info_alias_resolves_to_webonyx_resolve_info(): void {
- $this->assertTrue( class_exists( AliasedResolveInfo::class ) );
- $this->assertSame(
- ResolveInfo::class,
- ( new \ReflectionClass( AliasedResolveInfo::class ) )->getName(),
- 'The alias must resolve to the webonyx ResolveInfo class so resolver type hints accept what the engine passes.'
- );
- }
-
- /**
- * @testdox the StringValueNode alias resolves to the webonyx StringValueNode class.
- */
- public function test_string_value_node_alias_resolves_to_webonyx_string_value_node(): void {
- $this->assertTrue( class_exists( AliasedStringValueNode::class ) );
- $this->assertSame(
- StringValueNode::class,
- ( new \ReflectionClass( AliasedStringValueNode::class ) )->getName(),
- 'The alias must resolve to webonyx StringValueNode so custom-scalar parseLiteral() callbacks see the right type.'
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/SubclassesTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/SubclassesTest.php
deleted file mode 100644
index 6f558135b15..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/SubclassesTest.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure\Schema;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Error;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error as WebonyxError;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType as WebonyxCustomScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType as WebonyxEnumType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType as WebonyxInputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType as WebonyxInterfaceType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType as WebonyxObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema as WebonyxSchema;
-use WC_Unit_Test_Case;
-
-/**
- * Smoke tests for the no-op subclasses in the Schema/ surface. Each subclass
- * must extend its webonyx counterpart and accept the same configuration
- * payload, since generated code constructs them with webonyx-shaped configs.
- */
-class SubclassesTest extends WC_Unit_Test_Case {
- /**
- * @testdox ObjectType extends the webonyx ObjectType.
- */
- public function test_object_type_extends_webonyx_object_type(): void {
- $type = new ObjectType(
- array(
- 'name' => 'Foo',
- 'fields' => array(
- 'bar' => array( 'type' => Type::string() ),
- ),
- )
- );
-
- $this->assertInstanceOf( WebonyxObjectType::class, $type );
- $this->assertSame( 'Foo', $type->name );
- }
-
- /**
- * @testdox InputObjectType extends the webonyx InputObjectType.
- */
- public function test_input_object_type_extends_webonyx_input_object_type(): void {
- $type = new InputObjectType(
- array(
- 'name' => 'FooInput',
- 'fields' => array(
- 'bar' => array( 'type' => Type::string() ),
- ),
- )
- );
-
- $this->assertInstanceOf( WebonyxInputObjectType::class, $type );
- $this->assertSame( 'FooInput', $type->name );
- }
-
- /**
- * @testdox EnumType extends the webonyx EnumType.
- */
- public function test_enum_type_extends_webonyx_enum_type(): void {
- $type = new EnumType(
- array(
- 'name' => 'Status',
- 'values' => array(
- 'ACTIVE' => array( 'value' => 'active' ),
- 'INACTIVE' => array( 'value' => 'inactive' ),
- ),
- )
- );
-
- $this->assertInstanceOf( WebonyxEnumType::class, $type );
- $this->assertSame( 'Status', $type->name );
- }
-
- /**
- * @testdox InterfaceType extends the webonyx InterfaceType.
- */
- public function test_interface_type_extends_webonyx_interface_type(): void {
- $type = new InterfaceType(
- array(
- 'name' => 'Node',
- 'fields' => array(
- 'id' => array( 'type' => Type::nonNull( Type::int() ) ),
- ),
- )
- );
-
- $this->assertInstanceOf( WebonyxInterfaceType::class, $type );
- $this->assertSame( 'Node', $type->name );
- }
-
- /**
- * @testdox CustomScalarType extends the webonyx CustomScalarType.
- */
- public function test_custom_scalar_type_extends_webonyx_custom_scalar_type(): void {
- $type = new CustomScalarType(
- array(
- 'name' => 'MyDate',
- 'serialize' => static fn( $v ) => (string) $v,
- )
- );
-
- $this->assertInstanceOf( WebonyxCustomScalarType::class, $type );
- $this->assertSame( 'MyDate', $type->name );
- }
-
- /**
- * @testdox Schema extends the webonyx Schema and returns its query type.
- */
- public function test_schema_extends_webonyx_schema(): void {
- $query = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => array(
- 'hello' => array(
- 'type' => Type::string(),
- 'resolve' => static fn() => 'world',
- ),
- ),
- )
- );
-
- $schema = new Schema( array( 'query' => $query ) );
-
- $this->assertInstanceOf( WebonyxSchema::class, $schema );
- $this->assertSame( $query, $schema->getQueryType() );
- }
-
- /**
- * @testdox Error extends the webonyx Error and is ClientAware.
- */
- public function test_error_extends_webonyx_error_and_is_client_safe(): void {
- $error = new Error( 'visible to clients' );
-
- $this->assertInstanceOf( WebonyxError::class, $error );
- $this->assertInstanceOf( ClientAware::class, $error );
- $this->assertSame( 'visible to clients', $error->getMessage() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/TypeTest.php b/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/TypeTest.php
deleted file mode 100644
index 9f5d3e43dc5..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Infrastructure/Schema/TypeTest.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Infrastructure\Schema;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\BooleanType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FloatType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IDType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IntType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type as WebonyxType;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the {@see Type} static facade. Each scalar accessor must return
- * the matching webonyx singleton, and the modifiers must produce wrappers
- * around the supplied inner type.
- */
-class TypeTest extends WC_Unit_Test_Case {
- /**
- * @testdox int() returns the webonyx Int singleton.
- */
- public function test_int_returns_webonyx_int_singleton(): void {
- $this->assertInstanceOf( IntType::class, Type::int() );
- $this->assertSame( WebonyxType::int(), Type::int() );
- }
-
- /**
- * @testdox string() returns the webonyx String singleton.
- */
- public function test_string_returns_webonyx_string_singleton(): void {
- $this->assertInstanceOf( StringType::class, Type::string() );
- $this->assertSame( WebonyxType::string(), Type::string() );
- }
-
- /**
- * @testdox boolean() returns the webonyx Boolean singleton.
- */
- public function test_boolean_returns_webonyx_boolean_singleton(): void {
- $this->assertInstanceOf( BooleanType::class, Type::boolean() );
- $this->assertSame( WebonyxType::boolean(), Type::boolean() );
- }
-
- /**
- * @testdox float() returns the webonyx Float singleton.
- */
- public function test_float_returns_webonyx_float_singleton(): void {
- $this->assertInstanceOf( FloatType::class, Type::float() );
- $this->assertSame( WebonyxType::float(), Type::float() );
- }
-
- /**
- * @testdox id() returns the webonyx ID singleton.
- */
- public function test_id_returns_webonyx_id_singleton(): void {
- $this->assertInstanceOf( IDType::class, Type::id() );
- $this->assertSame( WebonyxType::id(), Type::id() );
- }
-
- /**
- * @testdox nonNull() wraps an inner type.
- */
- public function test_non_null_wraps_an_inner_type(): void {
- $wrapped = Type::nonNull( Type::string() );
-
- $this->assertInstanceOf( NonNull::class, $wrapped );
- $this->assertSame( Type::string(), $wrapped->getWrappedType() );
- }
-
- /**
- * @testdox listOf() wraps an inner type.
- */
- public function test_list_of_wraps_an_inner_type(): void {
- $wrapped = Type::listOf( Type::int() );
-
- $this->assertInstanceOf( ListOfType::class, $wrapped );
- $this->assertSame( Type::int(), $wrapped->getWrappedType() );
- }
-
- /**
- * @testdox modifiers compose into nested wrappers.
- */
- public function test_modifiers_compose(): void {
- $type = Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) );
-
- $this->assertInstanceOf( NonNull::class, $type );
- $inner = $type->getWrappedType();
- $this->assertInstanceOf( ListOfType::class, $inner );
- $this->assertInstanceOf( NonNull::class, $inner->getWrappedType() );
- $this->assertSame( Type::int(), $inner->getWrappedType()->getWrappedType() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php
deleted file mode 100644
index 4a4c043e1b4..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/CreateCouponTest.php
+++ /dev/null
@@ -1,192 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\InputTypes\Coupons\CreateCouponInput;
-use Automattic\WooCommerce\Api\Mutations\Coupons\CreateCoupon;
-use WC_Coupon;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see CreateCoupon}.
- */
-class CreateCouponTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var CreateCoupon
- */
- private CreateCoupon $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new CreateCoupon();
- }
-
- /**
- * @testdox execute() creates a coupon with the given code and returns its DTO.
- */
- public function test_execute_creates_coupon_with_required_fields(): void {
- $input = new CreateCouponInput();
- $input->code = 'welcome-2026';
-
- $result = $this->sut->execute( $input );
-
- $this->assertIsObject( $result );
- $this->assertSame( 'welcome-2026', $result->code );
- $this->assertGreaterThan( 0, $result->id );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'welcome-2026', $wc_coupon->get_code() );
- }
-
- /**
- * @testdox execute() persists optional scalar fields when provided.
- */
- public function test_execute_persists_optional_scalar_fields(): void {
- $input = new CreateCouponInput();
- $input->code = 'spring-sale';
- $input->description = 'Spring sale discount.';
- $input->amount = 15.5;
- $input->individual_use = true;
- $input->usage_limit = 100;
- $input->usage_limit_per_user = 1;
- $input->limit_usage_to_x_items = 3;
- $input->free_shipping = true;
- $input->exclude_sale_items = true;
- $input->minimum_amount = 20.0;
- $input->maximum_amount = 200.0;
- $input->date_expires = '2026-12-31T23:59:59+00:00';
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'Spring sale discount.', $wc_coupon->get_description() );
- $this->assertSame( '15.5', $wc_coupon->get_amount() );
- $this->assertTrue( $wc_coupon->get_individual_use() );
- $this->assertSame( 100, $wc_coupon->get_usage_limit() );
- $this->assertSame( 1, $wc_coupon->get_usage_limit_per_user() );
- $this->assertSame( 3, $wc_coupon->get_limit_usage_to_x_items() );
- $this->assertTrue( $wc_coupon->get_free_shipping() );
- $this->assertTrue( $wc_coupon->get_exclude_sale_items() );
- $this->assertSame( '20', $wc_coupon->get_minimum_amount() );
- $this->assertSame( '200', $wc_coupon->get_maximum_amount() );
- $this->assertSame( '2026-12-31', $wc_coupon->get_date_expires()->format( 'Y-m-d' ) );
- }
-
- /**
- * @testdox execute() persists array fields (product/category IDs, email restrictions).
- */
- public function test_execute_persists_array_fields(): void {
- $input = new CreateCouponInput();
- $input->code = 'array-coupon';
- $input->product_ids = array( 10, 20 );
- $input->excluded_product_ids = array( 30 );
- $input->product_categories = array( 1 );
- $input->excluded_product_categories = array( 2, 3 );
- $input->email_restrictions = array( 'foo@example.com', 'bar@example.com' );
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( array( 10, 20 ), $wc_coupon->get_product_ids() );
- $this->assertSame( array( 30 ), $wc_coupon->get_excluded_product_ids() );
- $this->assertSame( array( 1 ), $wc_coupon->get_product_categories() );
- $this->assertSame( array( 2, 3 ), $wc_coupon->get_excluded_product_categories() );
- $this->assertSame( array( 'foo@example.com', 'bar@example.com' ), $wc_coupon->get_email_restrictions() );
- }
-
- /**
- * @testdox execute() applies the discount_type enum when provided.
- */
- public function test_execute_applies_discount_type_enum(): void {
- $input = new CreateCouponInput();
- $input->code = 'percent-off';
- $input->discount_type = DiscountType::Percent;
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'percent', $wc_coupon->get_discount_type() );
- }
-
- /**
- * @testdox execute() persists fixed discount type enums when provided.
- *
- * @dataProvider fixed_discount_type_provider
- *
- * @param DiscountType $discount_type The discount type enum.
- * @param string $expected The expected stored discount type.
- */
- public function test_execute_persists_fixed_discount_type_enums( DiscountType $discount_type, string $expected ): void {
- $input = new CreateCouponInput();
- $input->code = 'coupon-' . str_replace( '_', '-', $expected );
- $input->discount_type = $discount_type;
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( $expected, $wc_coupon->get_discount_type() );
- }
-
- /**
- * Data provider for fixed discount types.
- *
- * @return array<string, array{0: DiscountType, 1: string}>
- */
- public function fixed_discount_type_provider(): array {
- return array(
- 'fixed cart' => array( DiscountType::FixedCart, 'fixed_cart' ),
- 'fixed product' => array( DiscountType::FixedProduct, 'fixed_product' ),
- );
- }
-
- /**
- * @testdox execute() skips set_discount_type() when discount_type is null.
- */
- public function test_execute_skips_discount_type_when_provided_null(): void {
- $input = new CreateCouponInput();
- $input->code = 'null-discount-type';
- $input->discount_type = null;
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'fixed_cart', $wc_coupon->get_discount_type() );
- }
-
- /**
- * @testdox execute() applies the status enum when provided.
- */
- public function test_execute_applies_status_enum(): void {
- $input = new CreateCouponInput();
- $input->code = 'draft-coupon';
- $input->status = CouponStatus::Draft;
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'draft', $wc_coupon->get_status() );
- }
-
- /**
- * @testdox execute() skips set_status() when status is null.
- */
- public function test_execute_skips_status_when_provided_null(): void {
- $input = new CreateCouponInput();
- $input->code = 'null-status';
- $input->status = null;
-
- $result = $this->sut->execute( $input );
-
- $wc_coupon = new WC_Coupon( $result->id );
- $this->assertSame( 'publish', $wc_coupon->get_status() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/DeleteCouponTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/DeleteCouponTest.php
deleted file mode 100644
index 32034a1e7a8..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/DeleteCouponTest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Mutations\Coupons\DeleteCoupon;
-use Automattic\WooCommerce\Api\Types\Coupons\DeleteCouponResult;
-use WC_Helper_Coupon;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see DeleteCoupon}.
- */
-class DeleteCouponTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var DeleteCoupon
- */
- private DeleteCoupon $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new DeleteCoupon();
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_pre_delete_data' );
- parent::tearDown();
- }
-
- /**
- * @testdox execute() throws NOT_FOUND when the coupon ID does not exist.
- */
- public function test_execute_throws_not_found_for_missing_coupon(): void {
- try {
- $this->sut->execute( 999999 );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Coupon not found.', $e->getMessage() );
- $this->assertSame( 'NOT_FOUND', $e->getErrorCode() );
- $this->assertSame( 404, $e->getStatusCode() );
- }
- }
-
- /**
- * @testdox execute() trashes the coupon when force=false and returns DeleteCouponResult.
- */
- public function test_execute_trashes_coupon_without_force(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'to-trash' );
- $id = $coupon->get_id();
-
- $result = $this->sut->execute( $id, false );
-
- $this->assertInstanceOf( DeleteCouponResult::class, $result );
- $this->assertSame( $id, $result->id );
- $this->assertTrue( $result->deleted );
- $this->assertSame( 'trash', get_post_status( $id ) );
- }
-
- /**
- * @testdox execute() permanently deletes the coupon when force=true.
- */
- public function test_execute_force_deletes_coupon(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'to-delete' );
- $id = $coupon->get_id();
-
- $result = $this->sut->execute( $id, true );
-
- $this->assertInstanceOf( DeleteCouponResult::class, $result );
- $this->assertSame( $id, $result->id );
- $this->assertTrue( $result->deleted );
- $this->assertNull( get_post( $id ) );
- }
-
- /**
- * @testdox execute() defaults to non-force deletion (trash).
- */
- public function test_execute_defaults_to_trash(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'default-trash' );
- $id = $coupon->get_id();
-
- $result = $this->sut->execute( $id );
-
- $this->assertInstanceOf( DeleteCouponResult::class, $result );
- $this->assertSame( $id, $result->id );
- $this->assertTrue( $result->deleted );
- $this->assertSame( 'trash', get_post_status( $id ) );
- }
-
- /**
- * @testdox execute() returns deleted=false when woocommerce_pre_delete_data short-circuits to false.
- */
- public function test_execute_returns_false_when_pre_delete_filter_returns_false(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'kept' );
- $id = $coupon->get_id();
-
- add_filter( 'woocommerce_pre_delete_data', '__return_false' );
-
- $result = $this->sut->execute( $id, true );
-
- $this->assertInstanceOf( DeleteCouponResult::class, $result );
- $this->assertSame( $id, $result->id );
- $this->assertFalse( $result->deleted );
- }
-
- /**
- * @testdox execute() surfaces a WP_Error from woocommerce_pre_delete_data as an INTERNAL_ERROR ApiException.
- */
- public function test_execute_translates_wp_error_to_api_exception(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'failing' );
-
- add_filter(
- 'woocommerce_pre_delete_data',
- static function () {
- return new \WP_Error( 'wc_delete_failed', 'Coupon delete failed.' );
- }
- );
-
- try {
- $this->sut->execute( $coupon->get_id(), true );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Coupon delete failed.', $e->getMessage() );
- $this->assertSame( 'INTERNAL_ERROR', $e->getErrorCode() );
- $this->assertSame( 500, $e->getStatusCode() );
- }
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/UpdateCouponTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/UpdateCouponTest.php
deleted file mode 100644
index 05ea8168da7..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Coupons/UpdateCouponTest.php
+++ /dev/null
@@ -1,201 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Coupons;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType;
-use Automattic\WooCommerce\Api\InputTypes\Coupons\UpdateCouponInput;
-use Automattic\WooCommerce\Api\Mutations\Coupons\UpdateCoupon;
-use WC_Coupon;
-use WC_Helper_Coupon;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see UpdateCoupon}.
- */
-class UpdateCouponTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var UpdateCoupon
- */
- private UpdateCoupon $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new UpdateCoupon();
- }
-
- /**
- * @testdox execute() throws NOT_FOUND when the coupon ID does not exist.
- */
- public function test_execute_throws_not_found_for_missing_coupon(): void {
- $input = new UpdateCouponInput();
- $input->id = 999999;
-
- try {
- $this->sut->execute( $input );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Coupon not found.', $e->getMessage() );
- $this->assertSame( 'NOT_FOUND', $e->getErrorCode() );
- $this->assertSame( 404, $e->getStatusCode() );
- }
- }
-
- /**
- * @testdox execute() updates only fields that were marked provided.
- */
- public function test_execute_updates_only_provided_fields(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'original-code' );
- $coupon->set_description( 'Original description.' );
- $coupon->save();
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->description = 'Updated description.';
- $input->mark_provided( 'description' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'Updated description.', $reloaded->get_description() );
- $this->assertSame( 'original-code', $reloaded->get_code() );
- }
-
- /**
- * @testdox execute() does not touch fields that were not marked provided, even if set on the DTO.
- */
- public function test_execute_ignores_fields_not_marked_provided(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'original-code' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->code = 'should-not-apply';
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'original-code', $reloaded->get_code() );
- }
-
- /**
- * @testdox execute() applies a non-null discount_type enum.
- */
- public function test_execute_applies_discount_type_enum(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'fixed-cart-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->discount_type = DiscountType::Percent;
- $input->mark_provided( 'discount_type' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'percent', $reloaded->get_discount_type() );
- }
-
- /**
- * @testdox execute() skips set_discount_type() when discount_type is provided as null.
- */
- public function test_execute_skips_discount_type_when_provided_null(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'fixed-cart-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->discount_type = null;
- $input->mark_provided( 'discount_type' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'fixed_cart', $reloaded->get_discount_type() );
- }
-
- /**
- * @testdox execute() applies a non-null status enum.
- */
- public function test_execute_applies_status_enum(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'status-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->status = CouponStatus::Draft;
- $input->mark_provided( 'status' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'draft', $reloaded->get_status() );
- }
-
- /**
- * @testdox execute() skips set_status() when status is provided as null.
- */
- public function test_execute_skips_status_when_provided_null(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'status-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->status = null;
- $input->mark_provided( 'status' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( 'publish', $reloaded->get_status() );
- }
-
- /**
- * @testdox execute() updates scalar amount fields when marked provided.
- */
- public function test_execute_updates_amount(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'amount-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->amount = 25.5;
- $input->mark_provided( 'amount' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( '25.5', $reloaded->get_amount() );
- }
-
- /**
- * @testdox execute() updates array fields when marked provided.
- */
- public function test_execute_updates_array_fields(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'array-update-coupon' );
-
- $input = new UpdateCouponInput();
- $input->id = $coupon->get_id();
- $input->product_ids = array( 100, 200 );
- $input->excluded_product_ids = array( 300 );
- $input->product_categories = array( 4 );
- $input->excluded_product_categories = array( 5, 6 );
- $input->email_restrictions = array( 'a@example.com' );
- $input->mark_provided( 'product_ids' );
- $input->mark_provided( 'excluded_product_ids' );
- $input->mark_provided( 'product_categories' );
- $input->mark_provided( 'excluded_product_categories' );
- $input->mark_provided( 'email_restrictions' );
-
- $this->sut->execute( $input );
-
- $reloaded = new WC_Coupon( $coupon->get_id() );
- $this->assertSame( array( 100, 200 ), $reloaded->get_product_ids() );
- $this->assertSame( array( 300 ), $reloaded->get_excluded_product_ids() );
- $this->assertSame( array( 4 ), $reloaded->get_product_categories() );
- $this->assertSame( array( 5, 6 ), $reloaded->get_excluded_product_categories() );
- $this->assertSame( array( 'a@example.com' ), $reloaded->get_email_restrictions() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/CreateProductTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Products/CreateProductTest.php
deleted file mode 100644
index e32815f2e64..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/CreateProductTest.php
+++ /dev/null
@@ -1,157 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\InputTypes\Products\CreateProductInput;
-use Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput;
-use Automattic\WooCommerce\Api\Mutations\Products\CreateProduct;
-use Automattic\WooCommerce\Api\Utils\Products\ProductRepository;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see CreateProduct}.
- */
-class CreateProductTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var CreateProduct
- */
- private CreateProduct $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new CreateProduct();
- $this->sut->init( new ProductRepository() );
- }
-
- /**
- * @testdox execute() creates a product with the given name and returns its DTO.
- */
- public function test_execute_creates_product_with_required_fields(): void {
- $input = new CreateProductInput();
- $input->name = 'Brand New Widget';
-
- $result = $this->sut->execute( $input );
-
- $this->assertIsObject( $result );
- $this->assertSame( 'Brand New Widget', $result->name );
- $this->assertGreaterThan( 0, $result->id );
-
- $wc_product = wc_get_product( $result->id );
- $this->assertInstanceOf( \WC_Product::class, $wc_product );
- $this->assertSame( 'Brand New Widget', $wc_product->get_name() );
- }
-
- /**
- * @testdox execute() persists optional scalar fields when provided.
- */
- public function test_execute_persists_optional_scalar_fields(): void {
- $input = new CreateProductInput();
- $input->name = 'Detailed Widget';
- $input->slug = 'detailed-widget';
- $input->sku = 'SKU-DETAILED-001';
- $input->description = 'The long description.';
- $input->short_description = 'Short blurb.';
- $input->regular_price = 19.99;
- $input->sale_price = 14.99;
- $input->manage_stock = true;
- $input->stock_quantity = 42;
-
- $result = $this->sut->execute( $input );
-
- $wc_product = wc_get_product( $result->id );
- $this->assertSame( 'detailed-widget', $wc_product->get_slug() );
- $this->assertSame( 'SKU-DETAILED-001', $wc_product->get_sku() );
- $this->assertSame( 'The long description.', $wc_product->get_description() );
- $this->assertSame( 'Short blurb.', $wc_product->get_short_description() );
- $this->assertSame( '19.99', $wc_product->get_regular_price() );
- $this->assertSame( '14.99', $wc_product->get_sale_price() );
- $this->assertTrue( $wc_product->get_manage_stock() );
- $this->assertSame( 42, $wc_product->get_stock_quantity() );
- }
-
- /**
- * @testdox execute() applies the status enum when provided.
- */
- public function test_execute_applies_status_enum(): void {
- $input = new CreateProductInput();
- $input->name = 'Draft Widget';
- $input->status = ProductStatus::Draft;
-
- $result = $this->sut->execute( $input );
-
- $wc_product = wc_get_product( $result->id );
- $this->assertSame( 'draft', $wc_product->get_status() );
- }
-
- /**
- * @testdox execute() applies dimension fields when a DimensionsInput is provided.
- */
- public function test_execute_applies_dimensions(): void {
- $dimensions = new DimensionsInput();
- $dimensions->length = 10.5;
- $dimensions->width = 5.25;
- $dimensions->height = 2.0;
- $dimensions->weight = 1.5;
-
- $input = new CreateProductInput();
- $input->name = 'Boxed Widget';
- $input->dimensions = $dimensions;
-
- $result = $this->sut->execute( $input );
-
- $wc_product = wc_get_product( $result->id );
- $this->assertSame( '10.5', $wc_product->get_length() );
- $this->assertSame( '5.25', $wc_product->get_width() );
- $this->assertSame( '2', $wc_product->get_height() );
- $this->assertSame( '1.5', $wc_product->get_weight() );
- }
-
- /**
- * @testdox execute() throws VALIDATION_ERROR when the product name is already taken.
- */
- public function test_execute_rejects_duplicate_name(): void {
- WC_Helper_Product::create_simple_product( true, array( 'name' => 'Duplicate Widget' ) );
-
- $input = new CreateProductInput();
- $input->name = 'Duplicate Widget';
-
- try {
- $this->sut->execute( $input );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'A product with this name already exists.', $e->getMessage() );
- $this->assertSame( 'VALIDATION_ERROR', $e->getErrorCode() );
- $this->assertSame( 422, $e->getStatusCode() );
- $this->assertArrayHasKey( 'field', $e->getExtensions() );
- $this->assertSame( 'name', $e->getExtensions()['field'] );
- }
- }
-
- /**
- * @testdox execute() allows reusing the name of a trashed product.
- */
- public function test_execute_allows_reusing_trashed_product_name(): void {
- $existing = WC_Helper_Product::create_simple_product( true, array( 'name' => 'Trashed Widget' ) );
- $existing_id = $existing->get_id();
- $existing->delete( false );
-
- $input = new CreateProductInput();
- $input->name = 'Trashed Widget';
-
- $result = $this->sut->execute( $input );
-
- $this->assertIsObject( $result );
- $this->assertSame( 'Trashed Widget', $result->name );
- $this->assertNotSame( $existing_id, $result->id );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/DeleteProductTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Products/DeleteProductTest.php
deleted file mode 100644
index e09720e34c4..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/DeleteProductTest.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Mutations\Products\DeleteProduct;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see DeleteProduct}.
- */
-class DeleteProductTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var DeleteProduct
- */
- private DeleteProduct $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new DeleteProduct();
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_pre_delete_product' );
- parent::tearDown();
- }
-
- /**
- * @testdox execute() throws NOT_FOUND when the product ID does not exist.
- */
- public function test_execute_throws_not_found_for_missing_product(): void {
- try {
- $this->sut->execute( 999999 );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Product not found.', $e->getMessage() );
- $this->assertSame( 'NOT_FOUND', $e->getErrorCode() );
- $this->assertSame( 404, $e->getStatusCode() );
- }
- }
-
- /**
- * @testdox execute() trashes the product when force=false.
- */
- public function test_execute_trashes_product_without_force(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- $deleted = $this->sut->execute( $product->get_id(), false );
-
- $this->assertTrue( $deleted );
- $this->assertSame( 'trash', get_post_status( $product->get_id() ) );
- }
-
- /**
- * @testdox execute() permanently deletes the product when force=true.
- */
- public function test_execute_force_deletes_product(): void {
- $product = WC_Helper_Product::create_simple_product();
- $id = $product->get_id();
-
- $deleted = $this->sut->execute( $id, true );
-
- $this->assertTrue( $deleted );
- $this->assertNull( get_post( $id ) );
- }
-
- /**
- * @testdox execute() defaults to non-force deletion (trash).
- */
- public function test_execute_defaults_to_trash(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- $this->sut->execute( $product->get_id() );
-
- $this->assertSame( 'trash', get_post_status( $product->get_id() ) );
- }
-
- /**
- * @testdox execute() returns false when woocommerce_pre_delete_product short-circuits to false.
- */
- public function test_execute_returns_false_when_pre_delete_filter_returns_false(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- add_filter( 'woocommerce_pre_delete_product', '__return_false' );
-
- $this->assertFalse( $this->sut->execute( $product->get_id(), true ) );
- }
-
- /**
- * @testdox execute() surfaces a WP_Error from woocommerce_pre_delete_product as an INTERNAL_ERROR ApiException.
- */
- public function test_execute_translates_wp_error_to_api_exception(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- add_filter(
- 'woocommerce_pre_delete_product',
- static function () {
- return new \WP_Error( 'wc_delete_failed', 'Something went wrong.' );
- }
- );
-
- try {
- $this->sut->execute( $product->get_id(), true );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Something went wrong.', $e->getMessage() );
- $this->assertSame( 'INTERNAL_ERROR', $e->getErrorCode() );
- $this->assertSame( 500, $e->getStatusCode() );
- }
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/UpdateProductTest.php b/plugins/woocommerce/tests/php/src/Api/Mutations/Products/UpdateProductTest.php
deleted file mode 100644
index 1490d5c4c9a..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Mutations/Products/UpdateProductTest.php
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Mutations\Products;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\InputTypes\Products\DimensionsInput;
-use Automattic\WooCommerce\Api\InputTypes\Products\UpdateProductInput;
-use Automattic\WooCommerce\Api\Mutations\Products\UpdateProduct;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see UpdateProduct}.
- */
-class UpdateProductTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var UpdateProduct
- */
- private UpdateProduct $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new UpdateProduct();
- }
-
- /**
- * @testdox execute() throws NOT_FOUND when the product ID does not exist.
- */
- public function test_execute_throws_not_found_for_missing_product(): void {
- $input = new UpdateProductInput();
- $input->id = 999999;
-
- try {
- $this->sut->execute( $input );
- $this->fail( 'Expected ApiException was not thrown.' );
- } catch ( ApiException $e ) {
- $this->assertSame( 'Product not found.', $e->getMessage() );
- $this->assertSame( 'NOT_FOUND', $e->getErrorCode() );
- $this->assertSame( 404, $e->getStatusCode() );
- }
- }
-
- /**
- * @testdox execute() updates only fields that were marked provided.
- */
- public function test_execute_updates_only_provided_fields(): void {
- $product = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'name' => 'Original Name',
- 'description' => 'Original description.',
- )
- );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->name = 'Updated Name';
- $input->mark_provided( 'name' );
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( 'Updated Name', $reloaded->get_name() );
- $this->assertSame( 'Original description.', $reloaded->get_description() );
- }
-
- /**
- * @testdox execute() does not touch fields that were not marked provided, even if set on the DTO.
- */
- public function test_execute_ignores_fields_not_marked_provided(): void {
- $product = WC_Helper_Product::create_simple_product( true, array( 'name' => 'Original Name' ) );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->name = 'Should Not Apply';
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( 'Original Name', $reloaded->get_name() );
- }
-
- /**
- * @testdox execute() skips set_status() when status is provided as null.
- */
- public function test_execute_skips_status_when_provided_null(): void {
- $product = WC_Helper_Product::create_simple_product( true, array( 'status' => 'draft' ) );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->status = null;
- $input->mark_provided( 'status' );
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( 'draft', $reloaded->get_status() );
- }
-
- /**
- * @testdox execute() applies a non-null status enum.
- */
- public function test_execute_applies_status_enum(): void {
- $product = WC_Helper_Product::create_simple_product( true, array( 'status' => 'draft' ) );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->status = ProductStatus::Published;
- $input->mark_provided( 'status' );
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( 'publish', $reloaded->get_status() );
- }
-
- /**
- * @testdox execute() clears a price when explicit null is provided.
- */
- public function test_execute_clears_price_when_explicit_null(): void {
- $product = WC_Helper_Product::create_simple_product( true, array( 'regular_price' => '19.99' ) );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->regular_price = null;
- $input->mark_provided( 'regular_price' );
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( '', $reloaded->get_regular_price() );
- }
-
- /**
- * @testdox execute() applies provided dimension fields and leaves others alone.
- */
- public function test_execute_applies_dimensions_selectively(): void {
- $product = WC_Helper_Product::create_simple_product();
- $product->set_length( '10' );
- $product->set_width( '5' );
- $product->save();
-
- $dimensions = new DimensionsInput();
- $dimensions->length = 20.0;
- $dimensions->mark_provided( 'length' );
-
- $input = new UpdateProductInput();
- $input->id = $product->get_id();
- $input->dimensions = $dimensions;
- $input->mark_provided( 'dimensions' );
-
- $this->sut->execute( $input );
-
- $reloaded = wc_get_product( $product->get_id() );
- $this->assertSame( '20', $reloaded->get_length() );
- $this->assertSame( '5', $reloaded->get_width() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/GetCouponTest.php b/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/GetCouponTest.php
deleted file mode 100644
index 9d7c4f0d4fe..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/GetCouponTest.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Queries\Coupons;
-
-use Automattic\WooCommerce\Api\Queries\Coupons\GetCoupon;
-use WC_Helper_Coupon;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see GetCoupon}.
- */
-class GetCouponTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var GetCoupon
- */
- private GetCoupon $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->sut = new GetCoupon();
- }
-
- /**
- * @testdox execute() throws InvalidArgumentException when neither id nor code is provided.
- */
- public function test_execute_rejects_missing_arguments(): void {
- $this->expectException( \InvalidArgumentException::class );
- $this->expectExceptionMessage( 'Exactly one of "id" or "code" must be provided.' );
-
- $this->sut->execute();
- }
-
- /**
- * @testdox execute() throws InvalidArgumentException when both id and code are provided.
- */
- public function test_execute_rejects_both_arguments(): void {
- $this->expectException( \InvalidArgumentException::class );
- $this->expectExceptionMessage( 'Exactly one of "id" or "code" must be provided.' );
-
- $this->sut->execute( id: 1, code: 'something' );
- }
-
- /**
- * @testdox execute() returns the mapped Coupon DTO for a valid ID.
- */
- public function test_execute_returns_coupon_for_valid_id(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'happy-friday' );
-
- $result = $this->sut->execute( id: $coupon->get_id() );
-
- $this->assertIsObject( $result );
- $this->assertSame( $coupon->get_id(), $result->id );
- $this->assertSame( 'happy-friday', $result->code );
- }
-
- /**
- * @testdox execute() returns the mapped Coupon DTO for a valid code.
- */
- public function test_execute_returns_coupon_for_valid_code(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'spring-sale' );
-
- $result = $this->sut->execute( code: 'spring-sale' );
-
- $this->assertIsObject( $result );
- $this->assertSame( $coupon->get_id(), $result->id );
- $this->assertSame( 'spring-sale', $result->code );
- }
-
- /**
- * @testdox execute() returns null when the ID does not exist.
- */
- public function test_execute_returns_null_for_missing_id(): void {
- $this->assertNull( $this->sut->execute( id: 999999 ) );
- }
-
- /**
- * @testdox execute() returns null when the code does not exist.
- */
- public function test_execute_returns_null_for_missing_code(): void {
- $this->assertNull( $this->sut->execute( code: 'does-not-exist' ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/ListCouponsTest.php b/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/ListCouponsTest.php
deleted file mode 100644
index b35e44f45c8..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Queries/Coupons/ListCouponsTest.php
+++ /dev/null
@@ -1,230 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Queries\Coupons;
-
-use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus;
-use Automattic\WooCommerce\Api\Pagination\IdCursorFilter;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Api\Queries\Coupons\ListCoupons;
-use ReflectionClass;
-use WC_Helper_Coupon;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see ListCoupons}.
- */
-class ListCouponsTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var ListCoupons
- */
- private ListCoupons $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- // IdCursorFilter registers its posts_where hook once per request and
- // remembers it on a static flag. WP_UnitTestCase resets $wp_filter on
- // every tear_down(), so the actual hook is gone but the flag stays
- // true — leaving subsequent cursor tests without a working filter.
- $reflection = new ReflectionClass( IdCursorFilter::class );
- $property = $reflection->getProperty( 'registered' );
- $property->setAccessible( true );
- $property->setValue( null, false );
-
- $this->sut = new ListCoupons();
- }
-
- /**
- * @testdox execute() returns all coupons with ascending IDs by default.
- */
- public function test_execute_returns_all_coupons_in_ascending_order(): void {
- $a = WC_Helper_Coupon::create_coupon( 'a-coupon' );
- $b = WC_Helper_Coupon::create_coupon( 'b-coupon' );
- $c = WC_Helper_Coupon::create_coupon( 'c-coupon' );
-
- $connection = $this->sut->execute( new PaginationParams() );
-
- $this->assertSame( 3, $connection->total_count );
- $this->assertCount( 3, $connection->nodes );
- $this->assertSame( $a->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $b->get_id(), $connection->nodes[1]->id );
- $this->assertSame( $c->get_id(), $connection->nodes[2]->id );
- }
-
- /**
- * @testdox execute() honors `first` and signals has_next_page when more remain.
- */
- public function test_execute_paginates_forward_with_first(): void {
- WC_Helper_Coupon::create_coupon( 'a' );
- WC_Helper_Coupon::create_coupon( 'b' );
- WC_Helper_Coupon::create_coupon( 'c' );
-
- $connection = $this->sut->execute( new PaginationParams( first: 2 ) );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( 3, $connection->total_count );
- $this->assertTrue( $connection->page_info->has_next_page );
- $this->assertFalse( $connection->page_info->has_previous_page );
- }
-
- /**
- * @testdox execute() honors `after` and returns coupons with IDs > cursor.
- */
- public function test_execute_paginates_forward_with_after_cursor(): void {
- $first = WC_Helper_Coupon::create_coupon( 'a' );
- $mid = WC_Helper_Coupon::create_coupon( 'b' );
- $last = WC_Helper_Coupon::create_coupon( 'c' );
-
- $after_cursor = base64_encode( (string) $first->get_id() );
-
- $connection = $this->sut->execute(
- new PaginationParams( first: 10, after: $after_cursor )
- );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $mid->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $last->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_previous_page );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() honors `last` and returns the trailing page in ascending order.
- */
- public function test_execute_paginates_backward_with_last(): void {
- WC_Helper_Coupon::create_coupon( 'a' );
- $b = WC_Helper_Coupon::create_coupon( 'b' );
- $c = WC_Helper_Coupon::create_coupon( 'c' );
-
- $connection = $this->sut->execute( new PaginationParams( last: 2 ) );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $b->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $c->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_previous_page );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() honors `before` and reports has_next_page=true (more remain after the window).
- */
- public function test_execute_paginates_backward_with_before_cursor(): void {
- $a = WC_Helper_Coupon::create_coupon( 'a' );
- $b = WC_Helper_Coupon::create_coupon( 'b' );
- $c = WC_Helper_Coupon::create_coupon( 'c' );
-
- $before_cursor = base64_encode( (string) $c->get_id() );
-
- $connection = $this->sut->execute(
- new PaginationParams( last: 10, before: $before_cursor )
- );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $a->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $b->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() filters by status.
- */
- public function test_execute_filters_by_status(): void {
- $published = WC_Helper_Coupon::create_coupon( 'published-coupon' );
- $draft_id = wp_insert_post(
- array(
- 'post_title' => 'draft-coupon',
- 'post_type' => 'shop_coupon',
- 'post_status' => 'draft',
- )
- );
-
- $connection = $this->sut->execute( new PaginationParams(), CouponStatus::Draft );
-
- $this->assertSame( 1, $connection->total_count );
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $draft_id, $connection->nodes[0]->id );
- $this->assertNotSame( $published->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() reports total_count after filters, not the unfiltered total.
- */
- public function test_total_count_reflects_filters(): void {
- WC_Helper_Coupon::create_coupon( 'a' );
- WC_Helper_Coupon::create_coupon( 'b' );
- wp_insert_post(
- array(
- 'post_title' => 'draft',
- 'post_type' => 'shop_coupon',
- 'post_status' => 'draft',
- )
- );
-
- $connection = $this->sut->execute( new PaginationParams( first: 1 ), CouponStatus::Published );
-
- $this->assertSame( 2, $connection->total_count );
- $this->assertCount( 1, $connection->nodes );
- }
-
- /**
- * @testdox each edge carries a base64-encoded ID cursor.
- */
- public function test_edges_carry_base64_id_cursors(): void {
- $coupon = WC_Helper_Coupon::create_coupon( 'a' );
-
- $connection = $this->sut->execute( new PaginationParams() );
-
- $this->assertCount( 1, $connection->edges );
- $this->assertSame( base64_encode( (string) $coupon->get_id() ), $connection->edges[0]->cursor );
- $this->assertSame( $coupon->get_id(), $connection->edges[0]->node->id );
- }
-
- /**
- * @testdox start_cursor and end_cursor on page_info mirror the first and last edge cursors.
- */
- public function test_page_info_carries_start_and_end_cursors(): void {
- $first = WC_Helper_Coupon::create_coupon( 'a' );
- WC_Helper_Coupon::create_coupon( 'b' );
- $last = WC_Helper_Coupon::create_coupon( 'c' );
-
- $connection = $this->sut->execute( new PaginationParams() );
-
- $this->assertSame( base64_encode( (string) $first->get_id() ), $connection->page_info->start_cursor );
- $this->assertSame( base64_encode( (string) $last->get_id() ), $connection->page_info->end_cursor );
- }
-
- /**
- * @testdox an empty result set returns no edges and null start/end cursors.
- */
- public function test_empty_result_set(): void {
- $connection = $this->sut->execute( new PaginationParams() );
-
- $this->assertSame( 0, $connection->total_count );
- $this->assertSame( array(), $connection->edges );
- $this->assertSame( array(), $connection->nodes );
- $this->assertNull( $connection->page_info->start_cursor );
- $this->assertNull( $connection->page_info->end_cursor );
- $this->assertFalse( $connection->page_info->has_next_page );
- $this->assertFalse( $connection->page_info->has_previous_page );
- }
-
- /**
- * @testdox first=N with exactly N matching coupons reports has_next_page=false.
- */
- public function test_first_equal_to_total_reports_no_next_page(): void {
- WC_Helper_Coupon::create_coupon( 'a' );
- WC_Helper_Coupon::create_coupon( 'b' );
-
- $connection = $this->sut->execute( new PaginationParams( first: 2 ) );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( 2, $connection->total_count );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Queries/Products/GetProductTest.php b/plugins/woocommerce/tests/php/src/Api/Queries/Products/GetProductTest.php
deleted file mode 100644
index 3c19cdad379..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Queries/Products/GetProductTest.php
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Queries\Products;
-
-use Automattic\WooCommerce\Api\Queries\Products\GetProduct;
-use Automattic\WooCommerce\Api\UnauthorizedException;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see GetProduct}.
- */
-class GetProductTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var GetProduct
- */
- private GetProduct $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- wp_set_current_user( 0 );
- $this->sut = new GetProduct();
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- wp_set_current_user( 0 );
- parent::tearDown();
- }
-
- /**
- * @testdox authorize() returns true for an admin reading any product.
- */
- public function test_authorize_allows_admin_for_any_product(): void {
- $author = self::factory()->user->create( array( 'role' => 'shop_manager' ) );
- $product = WC_Helper_Product::create_simple_product();
- wp_update_post(
- array(
- 'ID' => $product->get_id(),
- 'post_author' => $author,
- )
- );
-
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $this->assertTrue( $this->sut->authorize( $product->get_id(), false ) );
- }
-
- /**
- * @testdox authorize() returns true when the caller owns the product.
- */
- public function test_authorize_allows_owner(): void {
- $user = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- $product = WC_Helper_Product::create_simple_product();
- wp_update_post(
- array(
- 'ID' => $product->get_id(),
- 'post_author' => $user,
- )
- );
- wp_set_current_user( $user );
-
- $this->assertTrue( $this->sut->authorize( $product->get_id(), false ) );
- }
-
- /**
- * @testdox authorize() returns true when _preauthorized is true and the product exists.
- */
- public function test_authorize_honors_preauthorized_flag(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- $this->assertTrue( $this->sut->authorize( $product->get_id(), true ) );
- }
-
- /**
- * @testdox authorize() still throws for a non-existent ID even when _preauthorized is true.
- */
- public function test_authorize_rejects_missing_product_even_when_preauthorized(): void {
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( 999999, true );
- }
-
- /**
- * @testdox authorize() still throws for a non-product post even when _preauthorized is true.
- */
- public function test_authorize_rejects_non_product_post_even_when_preauthorized(): void {
- $post_id = self::factory()->post->create();
-
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( $post_id, true );
- }
-
- /**
- * @testdox authorize() throws "Product not found." for a non-positive ID.
- *
- * @dataProvider provider_non_positive_ids
- *
- * @param int $id The non-positive ID to reject.
- */
- public function test_authorize_rejects_non_positive_id( int $id ): void {
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( $id, false );
- }
-
- /**
- * @return array<string, array{int}>
- */
- public function provider_non_positive_ids(): array {
- return array(
- 'zero' => array( 0 ),
- 'negative' => array( -1 ),
- );
- }
-
- /**
- * @testdox authorize() throws "Product not found." for a non-existent ID.
- */
- public function test_authorize_rejects_missing_product(): void {
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( 999999, false );
- }
-
- /**
- * @testdox authorize() throws "Product not found." for a non-product post.
- */
- public function test_authorize_rejects_non_product_post(): void {
- $post_id = self::factory()->post->create();
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( $post_id, false );
- }
-
- /**
- * @testdox authorize() throws "Product not found." when a non-owner tries to read.
- */
- public function test_authorize_rejects_non_owner(): void {
- $owner = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- $other = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- $product = WC_Helper_Product::create_simple_product();
- wp_update_post(
- array(
- 'ID' => $product->get_id(),
- 'post_author' => $owner,
- )
- );
- wp_set_current_user( $other );
-
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( $product->get_id(), false );
- }
-
- /**
- * @testdox authorize() rejects an anonymous caller even when post_author is 0.
- */
- public function test_authorize_rejects_anonymous_caller_for_authorless_product(): void {
- $product = WC_Helper_Product::create_simple_product();
- wp_update_post(
- array(
- 'ID' => $product->get_id(),
- 'post_author' => 0,
- )
- );
-
- $this->expectException( UnauthorizedException::class );
- $this->expectExceptionMessage( 'Product not found.' );
-
- $this->sut->authorize( $product->get_id(), false );
- }
-
- /**
- * @testdox execute() returns a product DTO for a valid ID.
- */
- public function test_execute_returns_product_for_valid_id(): void {
- $product = WC_Helper_Product::create_simple_product( true, array( 'name' => 'Test Widget' ) );
-
- $result = $this->sut->execute( $product->get_id() );
-
- $this->assertIsObject( $result );
- $this->assertSame( $product->get_id(), $result->id );
- $this->assertSame( 'Test Widget', $result->name );
- }
-
- /**
- * @testdox execute() returns null for a non-positive ID.
- */
- public function test_execute_returns_null_for_non_positive_id(): void {
- $this->assertNull( $this->sut->execute( 0 ) );
- $this->assertNull( $this->sut->execute( -1 ) );
- }
-
- /**
- * @testdox execute() returns null when the ID does not point to a product.
- */
- public function test_execute_returns_null_for_non_product(): void {
- $post_id = self::factory()->post->create();
-
- $this->assertNull( $this->sut->execute( $post_id ) );
- $this->assertNull( $this->sut->execute( 999999 ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Api/Queries/Products/ListProductsTest.php b/plugins/woocommerce/tests/php/src/Api/Queries/Products/ListProductsTest.php
deleted file mode 100644
index bb25000d955..00000000000
--- a/plugins/woocommerce/tests/php/src/Api/Queries/Products/ListProductsTest.php
+++ /dev/null
@@ -1,361 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Api\Queries\Products;
-
-use Automattic\WooCommerce\Api\Enums\Products\ProductStatus;
-use Automattic\WooCommerce\Api\Enums\Products\ProductType;
-use Automattic\WooCommerce\Api\Enums\Products\StockStatus;
-use Automattic\WooCommerce\Api\InputTypes\Products\ProductFilterInput;
-use Automattic\WooCommerce\Api\Pagination\IdCursorFilter;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Api\Queries\Products\ListProducts;
-use ReflectionClass;
-use WC_Helper_Product;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for {@see ListProducts}.
- */
-class ListProductsTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var ListProducts
- */
- private ListProducts $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- // IdCursorFilter registers its posts_where hook once per request and
- // remembers it on a static flag. WP_UnitTestCase resets $wp_filter on
- // every tear_down(), so the actual hook is gone but the flag stays
- // true — leaving subsequent cursor tests without a working filter.
- $reflection = new ReflectionClass( IdCursorFilter::class );
- $property = $reflection->getProperty( 'registered' );
- $property->setAccessible( true );
- $property->setValue( null, false );
-
- $this->sut = new ListProducts();
- }
-
- /**
- * Build a {@see ProductFilterInput} with the given fields.
- *
- * @param ?ProductStatus $status Optional product status filter.
- * @param ?StockStatus $stock_status Optional stock status filter.
- * @param ?string $search Optional search keyword.
- */
- private function filters(
- ?ProductStatus $status = null,
- ?StockStatus $stock_status = null,
- ?string $search = null,
- ): ProductFilterInput {
- return new ProductFilterInput( $status, $stock_status, $search );
- }
-
- /**
- * @testdox execute() returns all products with ascending IDs by default.
- */
- public function test_execute_returns_all_products_in_ascending_order(): void {
- $a = WC_Helper_Product::create_simple_product();
- $b = WC_Helper_Product::create_simple_product();
- $c = WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams(), $this->filters() );
-
- $this->assertSame( 3, $connection->total_count );
- $this->assertCount( 3, $connection->nodes );
- $this->assertSame( $a->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $b->get_id(), $connection->nodes[1]->id );
- $this->assertSame( $c->get_id(), $connection->nodes[2]->id );
- }
-
- /**
- * @testdox execute() honors `first` and signals has_next_page when more remain.
- */
- public function test_execute_paginates_forward_with_first(): void {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams( first: 2 ), $this->filters() );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( 3, $connection->total_count );
- $this->assertTrue( $connection->page_info->has_next_page );
- $this->assertFalse( $connection->page_info->has_previous_page );
- }
-
- /**
- * @testdox execute() honors `after` and returns products with IDs > cursor.
- */
- public function test_execute_paginates_forward_with_after_cursor(): void {
- $first = WC_Helper_Product::create_simple_product();
- $mid = WC_Helper_Product::create_simple_product();
- $last = WC_Helper_Product::create_simple_product();
-
- $after_cursor = base64_encode( (string) $first->get_id() );
-
- $connection = $this->sut->execute(
- new PaginationParams( first: 10, after: $after_cursor ),
- $this->filters()
- );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $mid->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $last->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_previous_page );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() honors `last` and returns the trailing page in ascending order.
- */
- public function test_execute_paginates_backward_with_last(): void {
- $a = WC_Helper_Product::create_simple_product();
- $b = WC_Helper_Product::create_simple_product();
- $c = WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams( last: 2 ), $this->filters() );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $b->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $c->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_previous_page );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() honors `before` and reports has_next_page=true (more remain after the window).
- */
- public function test_execute_paginates_backward_with_before_cursor(): void {
- $a = WC_Helper_Product::create_simple_product();
- $b = WC_Helper_Product::create_simple_product();
- $c = WC_Helper_Product::create_simple_product();
-
- $before_cursor = base64_encode( (string) $c->get_id() );
-
- $connection = $this->sut->execute(
- new PaginationParams( last: 10, before: $before_cursor ),
- $this->filters()
- );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( $a->get_id(), $connection->nodes[0]->id );
- $this->assertSame( $b->get_id(), $connection->nodes[1]->id );
- $this->assertTrue( $connection->page_info->has_next_page );
- }
-
- /**
- * @testdox execute() filters by product status.
- */
- public function test_execute_filters_by_status(): void {
- WC_Helper_Product::create_simple_product( true, array( 'status' => 'publish' ) );
- $draft = WC_Helper_Product::create_simple_product( true, array( 'status' => 'draft' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( status: ProductStatus::Draft )
- );
-
- $this->assertSame( 1, $connection->total_count );
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $draft->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() filters by stock_status InStock.
- */
- public function test_execute_filters_by_stock_status_in_stock(): void {
- $in_stock = WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'instock' ) );
- WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'outofstock' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( stock_status: StockStatus::InStock )
- );
-
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $in_stock->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() filters by stock_status OutOfStock.
- */
- public function test_execute_filters_by_stock_status_out_of_stock(): void {
- WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'instock' ) );
- $out_of_stock = WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'outofstock' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( stock_status: StockStatus::OutOfStock )
- );
-
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $out_of_stock->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() filters by stock_status OnBackorder.
- */
- public function test_execute_filters_by_stock_status_on_backorder(): void {
- WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'instock' ) );
- $on_backorder = WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'onbackorder' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( stock_status: StockStatus::OnBackorder )
- );
-
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $on_backorder->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() filters by stock_status Other (non-standard values).
- */
- public function test_execute_filters_by_stock_status_other(): void {
- WC_Helper_Product::create_simple_product( true, array( 'stock_status' => 'instock' ) );
- $custom = WC_Helper_Product::create_simple_product();
- update_post_meta( $custom->get_id(), '_stock_status', 'plugin_custom' );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( stock_status: StockStatus::Other )
- );
-
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $custom->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() filters by product_type Simple.
- */
- public function test_execute_filters_by_product_type_simple(): void {
- $simple = WC_Helper_Product::create_simple_product();
- $external = WC_Helper_Product::create_external_product();
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters(),
- ProductType::Simple
- );
-
- $ids = array_map( static fn( $node ): int => $node->id, $connection->nodes );
- $this->assertContains( $simple->get_id(), $ids );
- $this->assertNotContains( $external->get_id(), $ids );
- }
-
- /**
- * @testdox execute() filters by product_type Other (non-standard types).
- */
- public function test_execute_filters_by_product_type_other(): void {
- $simple = WC_Helper_Product::create_simple_product();
- $custom = WC_Helper_Product::create_simple_product();
- wp_set_object_terms( $custom->get_id(), 'plugin_custom_type', 'product_type' );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters(),
- ProductType::Other
- );
-
- $ids = array_map( static fn( $node ): int => $node->id, $connection->nodes );
- $this->assertContains( $custom->get_id(), $ids );
- $this->assertNotContains( $simple->get_id(), $ids );
- }
-
- /**
- * @testdox execute() filters by search keyword against the product name.
- */
- public function test_execute_filters_by_search(): void {
- $widget = WC_Helper_Product::create_simple_product( true, array( 'name' => 'Blue Widget' ) );
- WC_Helper_Product::create_simple_product( true, array( 'name' => 'Red Gadget' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams(),
- $this->filters( search: 'Widget' )
- );
-
- $this->assertCount( 1, $connection->nodes );
- $this->assertSame( $widget->get_id(), $connection->nodes[0]->id );
- }
-
- /**
- * @testdox execute() reports total_count after filters, not the unfiltered total.
- */
- public function test_total_count_reflects_filters(): void {
- WC_Helper_Product::create_simple_product( true, array( 'status' => 'publish' ) );
- WC_Helper_Product::create_simple_product( true, array( 'status' => 'publish' ) );
- WC_Helper_Product::create_simple_product( true, array( 'status' => 'draft' ) );
-
- $connection = $this->sut->execute(
- new PaginationParams( first: 1 ),
- $this->filters( status: ProductStatus::Published )
- );
-
- $this->assertSame( 2, $connection->total_count );
- $this->assertCount( 1, $connection->nodes );
- }
-
- /**
- * @testdox each edge carries a base64-encoded ID cursor.
- */
- public function test_edges_carry_base64_id_cursors(): void {
- $product = WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams(), $this->filters() );
-
- $this->assertCount( 1, $connection->edges );
- $this->assertSame( base64_encode( (string) $product->get_id() ), $connection->edges[0]->cursor );
- $this->assertSame( $product->get_id(), $connection->edges[0]->node->id );
- }
-
- /**
- * @testdox start_cursor and end_cursor on page_info mirror the first and last edge cursors.
- */
- public function test_page_info_carries_start_and_end_cursors(): void {
- $first = WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_simple_product();
- $last = WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams(), $this->filters() );
-
- $this->assertSame( base64_encode( (string) $first->get_id() ), $connection->page_info->start_cursor );
- $this->assertSame( base64_encode( (string) $last->get_id() ), $connection->page_info->end_cursor );
- }
-
- /**
- * @testdox an empty result set returns no edges and null start/end cursors.
- */
- public function test_empty_result_set(): void {
- $connection = $this->sut->execute( new PaginationParams(), $this->filters() );
-
- $this->assertSame( 0, $connection->total_count );
- $this->assertSame( array(), $connection->edges );
- $this->assertSame( array(), $connection->nodes );
- $this->assertNull( $connection->page_info->start_cursor );
- $this->assertNull( $connection->page_info->end_cursor );
- $this->assertFalse( $connection->page_info->has_next_page );
- $this->assertFalse( $connection->page_info->has_previous_page );
- }
-
- /**
- * @testdox first=N with exactly N matching products reports has_next_page=false.
- */
- public function test_first_equal_to_total_reports_no_next_page(): void {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_simple_product();
-
- $connection = $this->sut->execute( new PaginationParams( first: 2 ), $this->filters() );
-
- $this->assertCount( 2, $connection->nodes );
- $this->assertSame( 2, $connection->total_count );
- $this->assertFalse( $connection->page_info->has_next_page );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/AuthorizationAttributeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/AuthorizationAttributeTest.php
deleted file mode 100644
index e16c493bc94..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/AuthorizationAttributeTest.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for the contract surface of the authorization attributes
- * shipped with the dual API ({@see PublicAccess} and
- * {@see RequiredCapability}). Pins the {@code #[Attribute]} flag set so
- * future widenings/narrowings are caught explicitly.
- */
-class AuthorizationAttributeTest extends WC_Unit_Test_Case {
- /**
- * @return array<string, array{class-string, int}>
- */
- public function provider_attribute_targets(): array {
- return array(
- 'PublicAccess accepts TARGET_CLASS' => array( PublicAccess::class, \Attribute::TARGET_CLASS ),
- 'PublicAccess accepts TARGET_PROPERTY' => array( PublicAccess::class, \Attribute::TARGET_PROPERTY ),
- 'RequiredCapability accepts TARGET_CLASS' => array( RequiredCapability::class, \Attribute::TARGET_CLASS ),
- 'RequiredCapability accepts TARGET_PROPERTY' => array( RequiredCapability::class, \Attribute::TARGET_PROPERTY ),
- );
- }
-
- /**
- * @testdox Authorization attribute accepts the target listed in the provider.
- *
- * @dataProvider provider_attribute_targets
- * @param class-string $attribute_class The attribute class under test.
- * @param int $target_flag A single {@see \Attribute} TARGET_* flag the class must accept.
- */
- public function test_attribute_accepts_target( string $attribute_class, int $target_flag ): void {
- $reflection = new \ReflectionClass( $attribute_class );
- $attributes = $reflection->getAttributes( \Attribute::class );
-
- $this->assertNotEmpty( $attributes, $attribute_class . ' should be decorated with #[Attribute].' );
-
- $attribute = $attributes[0]->newInstance();
- $this->assertNotSame(
- 0,
- $attribute->flags & $target_flag,
- $attribute_class . ' should accept the requested TARGET_* flag.'
- );
- }
-
- /**
- * @testdox RequiredCapability remains repeatable after the property-target widening.
- */
- public function test_required_capability_is_still_repeatable(): void {
- $reflection = new \ReflectionClass( RequiredCapability::class );
- $attributes = $reflection->getAttributes( \Attribute::class );
-
- $attribute = $attributes[0]->newInstance();
- $this->assertNotSame( 0, $attribute->flags & \Attribute::IS_REPEATABLE );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributeInheritanceTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributeInheritanceTest.php
deleted file mode 100644
index 971740c9fd9..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributeInheritanceTest.php
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-/**
- * Tests for capability/access-attribute inheritance.
- *
- * The builder treats #[RequiredCapability] / #[PublicAccess] as inheritable
- * from three sources: parent classes, traits, and PHP interfaces. A direct
- * attribute on the class itself takes precedence; otherwise caps from every
- * source are merged. These tests verify both the generated check list and
- * the runtime behaviour.
- */
-class AttributeInheritanceTest extends AutogeneratedTestCase {
- /**
- * Read the autogenerated resolver source for a given query class.
- *
- * @param string $class_name The short PHP class name (e.g. `InheritedCapQuery`).
- */
- private function read_generated_resolver( string $class_name ): string {
- $path = __DIR__ . '/../Fixtures/DummyApiAutogenerated/GraphQLQueries/' . $class_name . '.php';
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
- return (string) file_get_contents( $path );
- }
-
- /**
- * Assert that the generated resolver source contains an instantiation of
- * RequiredCapability with the given capability slug.
- *
- * Generated form: `( new \...\RequiredCapability( 'cap' ) )->authorize( ... )`.
- * Phrased loosely so it still matches if the linter shifts whitespace.
- *
- * @param string $capability The capability slug.
- * @param string $source The generated resolver source code.
- */
- private function assertGeneratesRequiredCapability( string $capability, string $source ): void {
- $this->assertMatchesRegularExpression(
- '/new \\\\Automattic\\\\WooCommerce\\\\Api\\\\Attributes\\\\RequiredCapability\\(\s*\'' . preg_quote( $capability, '/' ) . '\'\s*\\)/',
- $source
- );
- }
-
- /**
- * Assert that the generated resolver source does NOT contain an instantiation
- * of RequiredCapability with the given capability slug.
- *
- * @param string $capability The capability slug.
- * @param string $source The generated resolver source code.
- */
- private function assertDoesNotGenerateRequiredCapability( string $capability, string $source ): void {
- $this->assertDoesNotMatchRegularExpression(
- '/new \\\\Automattic\\\\WooCommerce\\\\Api\\\\Attributes\\\\RequiredCapability\\(\s*\'' . preg_quote( $capability, '/' ) . '\'\s*\\)/',
- $source
- );
- }
-
- /**
- * @testdox a query class inherits #[RequiredCapability] from its abstract parent.
- */
- public function test_required_capability_inherited_from_parent(): void {
- $source = $this->read_generated_resolver( 'InheritedCapQuery' );
- $this->assertGeneratesRequiredCapability( 'manage_options', $source );
-
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- $ok = $this->execute_query( '{ inheritedCap { result } }' );
- $this->assertSame( 'inherited cap', $ok['data']['inheritedCap']['result'] ?? null );
-
- wp_set_current_user( 0 );
- $rejected = $this->execute_query( '{ inheritedCap { result } }' );
- // Anonymous -> UNAUTHORIZED (the caller could authenticate to gain access).
- $this->assertSame( 'UNAUTHORIZED', $rejected['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a query class inherits #[PublicAccess] from a trait.
- */
- public function test_public_access_inherited_from_trait(): void {
- $source = $this->read_generated_resolver( 'InheritedPublicQuery' );
- // PublicAccess inheritance short-circuits the attribute expression to
- // `true`, so the generated resolver carries no RequiredCapability
- // instantiation.
- $this->assertStringNotContainsString( 'RequiredCapability(', $source );
-
- wp_set_current_user( 0 );
- $result = $this->execute_query( '{ inheritedPublic { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'inherited public', $result['data']['inheritedPublic']['result'] ?? null );
- }
-
- /**
- * @testdox a query class inherits #[RequiredCapability] from a PHP interface.
- */
- public function test_required_capability_inherited_from_interface(): void {
- $source = $this->read_generated_resolver( 'InheritedFromInterfaceQuery' );
- $this->assertGeneratesRequiredCapability( 'manage_options', $source );
-
- wp_set_current_user( 0 );
- $result = $this->execute_query( '{ inheritedFromInterface { result } }' );
- // Anonymous -> UNAUTHORIZED.
- $this->assertSame( 'UNAUTHORIZED', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a direct attribute overrides any inherited capability.
- */
- public function test_direct_attribute_overrides_inherited_capability(): void {
- $source = $this->read_generated_resolver( 'OverriddenCapQuery' );
- $this->assertGeneratesRequiredCapability( 'manage_categories', $source );
- $this->assertDoesNotGenerateRequiredCapability( 'manage_options', $source );
-
- // `editor` has manage_categories but NOT manage_options. If the
- // inherited cap were used, this would fail.
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
- $result = $this->execute_query( '{ overriddenCap { result } }' );
- $this->assertSame( 'overridden cap', $result['data']['overriddenCap']['result'] ?? null );
-
- // And the same editor still fails the inherited-cap variant — proving
- // the difference is real, not a permissive role.
- $inherited = $this->execute_query( '{ inheritedCap { result } }' );
- $this->assertSame( 'FORBIDDEN', $inherited['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox capabilities inherited from multiple sources are all enforced.
- */
- public function test_capabilities_from_multiple_sources_are_merged(): void {
- $source = $this->read_generated_resolver( 'MergedCapsQuery' );
- // Both the parent's manage_options and the trait's edit_posts must
- // be present in the generated checks.
- $this->assertGeneratesRequiredCapability( 'manage_options', $source );
- $this->assertGeneratesRequiredCapability( 'edit_posts', $source );
-
- // `editor` has edit_posts but NOT manage_options -> must fail.
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
- $rejected = $this->execute_query( '{ mergedCaps { result } }' );
- $this->assertSame( 'FORBIDDEN', $rejected['errors'][0]['extensions']['code'] ?? null );
-
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- $ok = $this->execute_query( '{ mergedCaps { result } }' );
- $this->assertSame( 'merged caps', $ok['data']['mergedCaps']['result'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributesTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributesTest.php
deleted file mode 100644
index eac43c9711c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AttributesTest.php
+++ /dev/null
@@ -1,250 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority as PriorityEnum;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Priority as PriorityEnumType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input\CreateWidget as CreateWidgetInputType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input\WidgetFilter as WidgetFilterInputType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Identifiable as IdentifiableInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Gadget as GadgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootMutationType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootQueryType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-
-/**
- * Tests that every code-API attribute is reflected in the autogenerated
- * artifacts. One assertion per attribute keeps any future regression
- * narrowly localized.
- */
-class AttributesTest extends AutogeneratedTestCase {
- /**
- * @testdox #[Description] on a class becomes the GraphQL description of the type.
- */
- public function test_description_attribute_is_emitted_on_type(): void {
- $this->assertSame( 'A dummy widget that exercises every output-type attribute', WidgetType::get()->description );
- }
-
- /**
- * @testdox #[Description] on a property becomes the GraphQL description of the field.
- */
- public function test_description_attribute_is_emitted_on_field(): void {
- $slug = WidgetType::get()->getField( 'slug' );
- $this->assertSame( 'A short slug', $slug->description );
- }
-
- /**
- * @testdox #[Name] at the class level renames the GraphQL type.
- */
- public function test_name_attribute_renames_classes(): void {
- // Object types: Gadget -> GadgetType (renamed), Widget unchanged.
- $this->assertSame( 'GadgetType', GadgetType::get()->name );
- $this->assertSame( 'Widget', WidgetType::get()->name );
-
- // Enums: Priority -> TaskPriority (renamed), Color unchanged.
- $this->assertSame( 'TaskPriority', PriorityEnumType::get()->name );
-
- // Input types: WidgetFilterInput -> WidgetFilterArgs (renamed), CreateWidgetInput unchanged.
- $this->assertSame( 'WidgetFilterArgs', WidgetFilterInputType::get()->name );
- $this->assertSame( 'CreateWidgetInput', CreateWidgetInputType::get()->name );
-
- // Interfaces: Identifiable -> HasId (renamed), Named unchanged.
- $this->assertSame( 'HasId', IdentifiableInterfaceType::get()->name );
- $this->assertSame( 'Named', NamedInterfaceType::get()->name );
-
- // Queries / mutations: Name renames the field on the root type.
- $query_fields = RootQueryType::get()->getFields();
- $this->assertArrayHasKey( 'widget', $query_fields );
- $this->assertArrayHasKey( 'widgets', $query_fields );
- $this->assertArrayHasKey( 'greeting', $query_fields );
- $this->assertArrayHasKey( 'namedThing', $query_fields );
- $this->assertArrayHasKey( 'failing', $query_fields );
-
- $mutation_fields = RootMutationType::get()->getFields();
- $this->assertArrayHasKey( 'increment', $mutation_fields );
- }
-
- /**
- * @testdox #[Name] at the enum-case level renames the GraphQL value.
- */
- public function test_name_attribute_renames_enum_cases(): void {
- $names = array_map( static fn( $v ) => $v->name, PriorityEnumType::get()->getValues() );
- // Cases without #[Name] follow the default SCREAMING_SNAKE_CASE conversion
- // (Low becomes LOW, High becomes HIGH); cases carrying #[Name(...)] use
- // that exact value (Normal carries #[Name('NORMAL_PRIORITY')]).
- $this->assertContains( 'LOW', $names );
- $this->assertContains( 'NORMAL_PRIORITY', $names );
- $this->assertContains( 'HIGH', $names );
- $this->assertNotContains( 'NORMAL', $names );
- }
-
- /**
- * @testdox #[Deprecated] on an enum case sets the deprecationReason.
- */
- public function test_deprecated_on_enum_case_sets_deprecation_reason(): void {
- $value = PriorityEnumType::get()->getValue( 'HIGH' );
- $this->assertSame( 'Use NORMAL_PRIORITY instead.', $value->deprecationReason );
- $this->assertTrue( $value->isDeprecated() );
- }
-
- /**
- * @testdox #[Deprecated] on a property sets the deprecationReason on the field.
- */
- public function test_deprecated_on_property_sets_deprecation_reason(): void {
- $field = WidgetType::get()->getField( 'legacy_price' );
- $this->assertSame( 'Use price instead.', $field->deprecationReason );
- $this->assertTrue( $field->isDeprecated() );
- }
-
- /**
- * @testdox #[Ignore] on a property removes the field from the schema.
- */
- public function test_ignore_on_property_drops_the_field(): void {
- $fields = WidgetType::get()->getFields();
- $this->assertArrayNotHasKey( 'internal_notes', $fields );
- }
-
- /**
- * @testdox #[ArrayOf] with a scalar produces a list of non-null scalars.
- */
- public function test_array_of_scalar_produces_list_of_non_null(): void {
- $field = WidgetType::get()->getField( 'tag_ids' );
- $type = $this->unwrap_non_null( $field->getType() );
- $this->assertInstanceOf( ListOfType::class, $type );
- $inner = $type->getWrappedType();
- $this->assertInstanceOf( NonNull::class, $inner );
- }
-
- /**
- * @testdox #[ArrayOf] with a class produces a list of that ObjectType.
- */
- public function test_array_of_class_produces_list_of_object_type(): void {
- $field = WidgetType::get()->getField( 'featured_reviews' );
- $type = $this->unwrap_non_null( $field->getType() );
- $this->assertInstanceOf( ListOfType::class, $type );
- $inner = $this->unwrap_non_null( $type->getWrappedType() );
- $this->assertSame( 'WidgetReview', $inner->name );
- }
-
- /**
- * @testdox #[ArrayOf] with an input type produces a list of that InputObjectType.
- */
- public function test_array_of_input_type_produces_list_of_input_object_type(): void {
- $field = RootMutationType::get()->getField( 'createWidget' );
- $arg = null;
- foreach ( $field->args as $candidate ) {
- if ( 'related_inputs' === $candidate->name ) {
- $arg = $candidate;
- break;
- }
- }
-
- $this->assertNotNull( $arg );
- $type = $this->unwrap_non_null( $arg->getType() );
- $this->assertInstanceOf( ListOfType::class, $type );
- $inner = $this->unwrap_non_null( $type->getWrappedType() );
- $this->assertSame( CreateWidgetInputType::get(), $inner );
- }
-
- /**
- * @testdox #[ConnectionOf] produces a NodeNameConnection field.
- */
- public function test_connection_of_produces_connection_type(): void {
- $field = WidgetType::get()->getField( 'reviews' );
- $type = $this->unwrap_non_null( $field->getType() );
- $this->assertSame( 'WidgetReviewConnection', $type->name );
- }
-
- /**
- * @testdox #[ScalarType] points the generated field at the custom scalar.
- */
- public function test_scalar_type_overrides_the_field_type(): void {
- $field = WidgetType::get()->getField( 'date_created' );
- $this->assertSame( 'DummyDateTime', $field->getType()->name );
- }
-
- /**
- * @testdox #[Parameter] adds a named argument to a field.
- */
- public function test_parameter_attribute_adds_field_argument(): void {
- $field = WidgetType::get()->getField( 'price' );
- $arg_defs = $field->args;
- $this->assertNotEmpty( $arg_defs );
-
- $arg_names = array_map( static fn( $a ) => $a->name, $arg_defs );
- $this->assertContains( 'formatted', $arg_names );
- }
-
- /**
- * @testdox #[ParameterDescription] sets the description of a #[Parameter]-declared argument.
- */
- public function test_parameter_description_sets_argument_description(): void {
- $field = WidgetType::get()->getField( 'price' );
- $args = $field->args;
- $arg = null;
- foreach ( $args as $candidate ) {
- if ( 'formatted' === $candidate->name ) {
- $arg = $candidate;
- break;
- }
- }
- $this->assertNotNull( $arg );
- $this->assertSame( 'When true, prepend a $ sign', $arg->description );
- }
-
- /**
- * @testdox #[PublicAccess] removes capability checks from the generated resolver.
- */
- public function test_public_access_skips_capability_checks(): void {
- // `greeting` is public; even an anonymous user can call it.
- wp_set_current_user( 0 );
- $result = $this->execute_query( '{ greeting { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'Hello, world!', $result['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox #[RequiredCapability] enforces the capability at resolve time.
- */
- public function test_required_capability_enforces_capability(): void {
- wp_set_current_user( 0 );
- $result = $this->execute_query( '{ widget(id: 1) { id } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- // Anonymous -> UNAUTHORIZED.
- $this->assertSame( 'UNAUTHORIZED', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox #[ReturnType] points the generated field at an interface type.
- */
- public function test_return_type_uses_interface_type(): void {
- $field = RootQueryType::get()->getField( 'namedThing' );
- $type = $this->unwrap_non_null( $field->getType() );
- $this->assertSame( 'Named', $type->name );
- }
-
- /**
- * @testdox enum values that lack #[Description] omit the description in the schema.
- */
- public function test_enum_value_without_description_has_no_description(): void {
- $value = PriorityEnumType::get()->getValue( 'NORMAL_PRIORITY' );
- // Either null or empty — the absence of #[Description] should not
- // fabricate one.
- $this->assertEmpty( $value->description );
- }
-
- /**
- * @testdox enum values backed by the PHP enum carry the underlying case as their value.
- */
- public function test_enum_value_is_backed_by_php_enum_case(): void {
- $value = PriorityEnumType::get()->getValue( 'HIGH' );
- $this->assertSame( PriorityEnum::High, $value->value );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizationQueryDiscoveryTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizationQueryDiscoveryTest.php
deleted file mode 100644
index 21587b96ab0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizationQueryDiscoveryTest.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Api\Utils\SchemaHandle;
-
-/**
- * End-to-end coverage for the `_apiMetadata.authorization` surface:
- *
- * - `Widget::$caption` carries `#[RequiredCapability('manage_woocommerce')]`
- * and no opt-out attribute, so its row exposes the descriptor.
- * - `Widget::$legacy_price` carries `#[HiddenFromMetadataQuery]`, which
- * hides the whole target — neither its metadata nor any
- * authorization descriptor reaches `_apiMetadata`.
- *
- * The discovery hide is independent of the runtime authorization gate:
- * a target hidden from `_apiMetadata` can still be gated at resolution
- * time (see {@see FieldAuthorizeTest} for the runtime coverage).
- */
-class AuthorizationQueryDiscoveryTest extends AutogeneratedTestCase {
- /**
- * @testdox find_metadata exposes authorization descriptors on a visible gated field.
- */
- public function test_find_metadata_returns_authorization_descriptors(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( type: 'Widget', field: 'caption' );
-
- $this->assertCount( 1, $rows );
- $this->assertArrayHasKey( 'authorization', $rows[0] );
-
- $attributes = array_column( $rows[0]['authorization'], 'attribute' );
- $this->assertContains( 'RequiredCapability', $attributes );
- }
-
- /**
- * @testdox RequiredCapability args round-trip through the descriptor.
- */
- public function test_descriptor_carries_constructor_args(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( type: 'Widget', field: 'caption' );
-
- $required_cap = null;
- foreach ( $rows[0]['authorization'] as $descriptor ) {
- if ( 'RequiredCapability' === $descriptor['attribute'] ) {
- $required_cap = $descriptor;
- break;
- }
- }
- $this->assertNotNull( $required_cap );
- $this->assertSame( array( 'manage_woocommerce' ), $required_cap['args'] );
- }
-
- /**
- * @testdox find_metadata filters by attribute short name, trimming non-matching descriptors.
- */
- public function test_find_metadata_filters_by_attribute(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( attribute: 'RequiredCapability' );
-
- $this->assertNotEmpty( $rows );
- foreach ( $rows as $row ) {
- foreach ( $row['authorization'] as $descriptor ) {
- $this->assertSame( 'RequiredCapability', $descriptor['attribute'] );
- }
- }
- }
-
- /**
- * @testdox A target with #[HiddenFromMetadataQuery] does not appear in _apiMetadata.
- */
- public function test_hidden_target_is_omitted_from_apimetadata(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( type: 'Widget', field: 'legacy_price' );
-
- $this->assertSame(
- array(),
- $rows,
- 'legacy_price carries #[HiddenFromMetadataQuery]; the row must not surface.'
- );
- }
-
- /**
- * @testdox The _apiMetadata endpoint surfaces authorization through the GraphQL response.
- */
- public function test_apimetadata_endpoint_returns_authorization(): void {
- // `_apiMetadata` is gated by the principal's `can_query_metadata()` /
- // `can_introspect()` ladder; the default WC Principal grants only on
- // `manage_woocommerce`, so the test runs as an admin to clear the gate.
- $admin = self::factory()->user->create_and_get( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin->ID );
-
- $result = $this->execute_query(
- '{ _apiMetadata(type: "Widget", field: "caption") { type field authorization { attribute args } } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $rows = $result['data']['_apiMetadata'];
- $this->assertCount( 1, $rows );
-
- $found = false;
- foreach ( $rows[0]['authorization'] as $entry ) {
- if ( 'RequiredCapability' === $entry['attribute'] ) {
- $found = true;
- $this->assertSame( array( 'manage_woocommerce' ), $entry['args'] );
- }
- }
- $this->assertTrue( $found, 'RequiredCapability should appear in the _apiMetadata authorization list.' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizeTest.php
deleted file mode 100644
index 3fc7308094b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AuthorizeTest.php
+++ /dev/null
@@ -1,130 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-/**
- * Tests for the authorize() method support: standalone, with #[Ignore],
- * combined with an inherited cap (override mode), and composed with a
- * direct cap via the $_preauthorized infrastructure parameter.
- */
-class AuthorizeTest extends AutogeneratedTestCase {
- /**
- * Read the autogenerated resolver source for a given query class.
- *
- * @param string $class_name The short PHP class name.
- */
- private function read_generated_resolver( string $class_name ): string {
- $path = __DIR__ . '/../Fixtures/DummyApiAutogenerated/GraphQLQueries/' . $class_name . '.php';
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
- return (string) file_get_contents( $path );
- }
-
- /**
- * @testdox authorize() is the sole guard when no auth attribute is declared.
- */
- public function test_authorize_only_query_uses_authorize_method(): void {
- $source = $this->read_generated_resolver( 'AuthorizeOnlyQuery' );
- $this->assertStringNotContainsString( 'check_current_user_can', $source );
- $this->assertStringContainsString( 'ResolverHelpers::authorize_command', $source );
-
- wp_set_current_user( 0 );
-
- $allowed = $this->execute_query( '{ authorizeOnly(allow: true) { result } }' );
- $this->assertSame( 'allowed', $allowed['data']['authorizeOnly']['result'] ?? null );
-
- $denied = $this->execute_query( '{ authorizeOnly(allow: false) { result } }' );
- // Anonymous principal + denied → UNAUTHORIZED (re-authenticating could change the outcome).
- $this->assertSame( 'UNAUTHORIZED', $denied['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox #[Ignore] on authorize() suppresses it; the cap check applies instead.
- */
- public function test_ignored_authorize_method_is_skipped(): void {
- $source = $this->read_generated_resolver( 'IgnoredAuthorizeQuery' );
- // The autodiscovered RequiredCapability attribute is wired in; authorize_command is not.
- $this->assertMatchesRegularExpression(
- '/new \\\\Automattic\\\\WooCommerce\\\\Api\\\\Attributes\\\\RequiredCapability\\(\s*\'manage_options\'\s*\\)/',
- $source
- );
- $this->assertStringNotContainsString( 'authorize_command', $source );
-
- // Even though the fixture's authorize() returns false, an admin must
- // succeed — confirming the method is skipped at generation time.
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- $ok = $this->execute_query( '{ ignoredAuthorize { result } }' );
- $this->assertSame( 'cap enforced', $ok['data']['ignoredAuthorize']['result'] ?? null );
-
- wp_set_current_user( 0 );
- $rejected = $this->execute_query( '{ ignoredAuthorize { result } }' );
- // Anonymous → UNAUTHORIZED.
- $this->assertSame( 'UNAUTHORIZED', $rejected['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a direct authorize() supersedes a cap inherited from a parent class.
- */
- public function test_direct_authorize_overrides_inherited_capability(): void {
- $source = $this->read_generated_resolver( 'OverriddenAuthorizeQuery' );
- // The inherited manage_options cap MUST NOT be turned into a check —
- // authorize() takes over as the sole guard.
- $this->assertStringNotContainsString( 'check_current_user_can', $source );
- $this->assertStringContainsString( 'ResolverHelpers::authorize_command', $source );
-
- // `editor` has edit_posts but not manage_options. authorize() returns
- // true → succeeds despite the inherited cap pointing to manage_options.
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
- $ok = $this->execute_query( '{ overriddenAuthorize { result } }' );
- $this->assertSame( 'authorize wins', $ok['data']['overriddenAuthorize']['result'] ?? null );
-
- // `subscriber` has neither cap → authorize() returns false → rejected.
- $subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- wp_set_current_user( $subscriber );
- $rejected = $this->execute_query( '{ overriddenAuthorize { result } }' );
- $this->assertSame( 'FORBIDDEN', $rejected['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox $_preauthorized carries the result of the attribute-driven cap check into authorize().
- */
- public function test_composed_authorize_receives_preauthorized_flag(): void {
- $source = $this->read_generated_resolver( 'ComposedAuthorizeQuery' );
- // The cap is NOT enforced as a standalone gate; instead its result is
- // passed to authorize() as `_preauthorized` via the generated
- // `compute_preauthorized()` helper that wraps the attribute call.
- $this->assertMatchesRegularExpression(
- '/\'_preauthorized\'\s*=>\s*self::compute_preauthorized\\(\s*\\$context\\[\'principal\'\\]\s*\\)/',
- $source
- );
- // And no standalone authorization gate — authorize() stays the sole
- // guard, with the attribute outcome flowing in via _preauthorized.
- $this->assertStringNotContainsString( 'Standalone authorization gate', $source );
- // The attribute call lives inside the compute_preauthorized() helper.
- $this->assertMatchesRegularExpression(
- '/public static function compute_preauthorized\\([^)]*\\): bool\\s*\\{\\s*return \\(\\s*new \\\\Automattic\\\\WooCommerce\\\\Api\\\\Attributes\\\\RequiredCapability\\(\s*\'manage_options\'\s*\\)\s*\\)->authorize\\(\s*\\$principal\s*\\);/',
- $source
- );
-
- // admin: has manage_options → _preauthorized=true → authorize returns true.
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- $admin_ok = $this->execute_query( '{ composedAuthorize { result } }' );
- $this->assertSame( 'composed', $admin_ok['data']['composedAuthorize']['result'] ?? null );
-
- // editor: lacks manage_options → _preauthorized=false, BUT has edit_posts
- // → authorize falls back to the secondary cap and still allows the call.
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
- $editor_ok = $this->execute_query( '{ composedAuthorize { result } }' );
- $this->assertSame( 'composed', $editor_ok['data']['composedAuthorize']['result'] ?? null );
-
- // anonymous: neither cap → authorize returns false → UNAUTHORIZED.
- wp_set_current_user( 0 );
- $rejected = $this->execute_query( '{ composedAuthorize { result } }' );
- $this->assertSame( 'UNAUTHORIZED', $rejected['errors'][0]['extensions']['code'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AutogeneratedTestCase.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AutogeneratedTestCase.php
deleted file mode 100644
index 305281f260a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/AutogeneratedTestCase.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootMutationType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootQueryType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\TypeRegistry;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
-use WC_Unit_Test_Case;
-
-/**
- * Base class for tests that exercise the dummy API's autogenerated artifacts.
- *
- * Provides:
- * - {@see self::build_schema()} — a Schema wired against the dummy
- * RootQueryType / RootMutationType / TypeRegistry.
- * - {@see self::execute_query()} — runs a query against that schema and
- * returns the result array (data + errors).
- * - {@see self::unwrap_non_null()} / {@see self::unwrap_list()} — helpers to
- * dig past the NonNull/ListOf wrappers without leaking them into individual
- * test assertions.
- */
-abstract class AutogeneratedTestCase extends WC_Unit_Test_Case {
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- parent::tearDown();
- }
-
- /**
- * Build the dummy schema from its autogenerated parts.
- */
- protected function build_schema(): Schema {
- return new Schema(
- array(
- 'query' => RootQueryType::get(),
- 'mutation' => RootMutationType::get(),
- 'types' => TypeRegistry::get_interface_implementors(),
- )
- );
- }
-
- /**
- * Execute a GraphQL query against the dummy schema.
- *
- * Mirrors {@see \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase::process_request()}
- * by populating `contextValue['principal']` with a {@see Principal} wrapping
- * {@see \wp_get_current_user()} (anonymous → underlying WP_User has ID=0).
- * Tests that need to run as a specific user should call `wp_set_current_user($id)`
- * before invoking this helper.
- *
- * @param string $query The query source.
- * @param array $vars Variable values.
- *
- * @return array{data?: ?array, errors?: array}
- */
- protected function execute_query( string $query, array $vars = array() ): array {
- $result = GraphQL::executeQuery(
- schema: $this->build_schema(),
- source: $query,
- contextValue: new \ArrayObject(
- array(
- 'principal' => new Principal( wp_get_current_user() ),
- )
- ),
- variableValues: $vars,
- );
- return $result->toArray();
- }
-
- /**
- * Strip a NonNull wrapper if present.
- *
- * @param mixed $type Webonyx type instance.
- */
- protected function unwrap_non_null( $type ) {
- return $type instanceof NonNull ? $type->getWrappedType() : $type;
- }
-
- /**
- * Strip a ListOf wrapper if present.
- *
- * @param mixed $type Webonyx type instance.
- */
- protected function unwrap_list( $type ) {
- return $type instanceof ListOfType ? $type->getWrappedType() : $type;
- }
-
- /**
- * Return the named field from an ObjectType / InputObjectType.
- *
- * @param ObjectType|InputObjectType $object_type The type to inspect.
- * @param string $field_name The field to look up.
- */
- protected function get_field( $object_type, string $field_name ) {
- return $object_type->getField( $field_name );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/CustomScalarsTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/CustomScalarsTest.php
deleted file mode 100644
index 1a0de897294..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/CustomScalarsTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\AST\StringValueNode;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Scalars\DummyDateTime as DummyDateTimeType;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
-
-/**
- * Tests for the custom-scalar generation: serialization round-trip, parseValue
- * for variables, parseLiteral for inline literals, and rejection of malformed
- * input.
- */
-class CustomScalarsTest extends AutogeneratedTestCase {
- /**
- * @testdox the custom scalar serializes a DateTimeImmutable to its ATOM representation.
- */
- public function test_serialize_returns_atom_string(): void {
- $dt = new \DateTimeImmutable( '2024-06-15T08:30:00+00:00' );
-
- $serialized = DummyDateTimeType::get()->serialize( $dt );
-
- $this->assertSame( '2024-06-15T08:30:00+00:00', $serialized );
- }
-
- /**
- * @testdox the custom scalar's parseValue accepts a valid ISO 8601 string.
- */
- public function test_parse_value_accepts_iso_8601(): void {
- $parsed = DummyDateTimeType::get()->parseValue( '2024-06-15T08:30:00+00:00' );
-
- $this->assertInstanceOf( \DateTimeImmutable::class, $parsed );
- $this->assertSame( '2024-06-15T08:30:00+00:00', $parsed->format( \DateTimeInterface::ATOM ) );
- }
-
- /**
- * @testdox the custom scalar's parseValue throws a GraphQL Error on malformed input.
- */
- public function test_parse_value_rejects_malformed_input(): void {
- $this->expectException( \Automattic\WooCommerce\Api\Infrastructure\Schema\Error::class );
- DummyDateTimeType::get()->parseValue( 'not-a-date' );
- }
-
- /**
- * @testdox the custom scalar's parseLiteral accepts a StringValueNode.
- */
- public function test_parse_literal_accepts_string_value_node(): void {
- $node = new StringValueNode( array( 'value' => '2024-06-15T08:30:00+00:00' ) );
- $parsed = DummyDateTimeType::get()->parseLiteral( $node );
-
- $this->assertInstanceOf( \DateTimeImmutable::class, $parsed );
- $this->assertSame( '2024-06-15T08:30:00+00:00', $parsed->format( \DateTimeInterface::ATOM ) );
- }
-
- /**
- * @testdox the custom scalar's parseLiteral rejects non-string AST nodes.
- */
- public function test_parse_literal_rejects_non_string_node(): void {
- $node = new IntValueNode( array( 'value' => '123' ) );
-
- $this->expectException( \Automattic\WooCommerce\Api\Infrastructure\Schema\Error::class );
- DummyDateTimeType::get()->parseLiteral( $node );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/EnumsAndInterfacesTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/EnumsAndInterfacesTest.php
deleted file mode 100644
index b885eeb4f5a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/EnumsAndInterfacesTest.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color as ColorEnum;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority as PriorityEnum;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorEnumType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Identifiable as IdentifiableInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Gadget as GadgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-
-/**
- * Tests for the enum and interface generation paths.
- */
-class EnumsAndInterfacesTest extends AutogeneratedTestCase {
- /**
- * @testdox enums emit one EnumType value per backing case in SCREAMING_SNAKE_CASE.
- */
- public function test_enum_emits_value_per_case(): void {
- $values = ColorEnumType::get()->getValues();
- $names = array_map( static fn( $v ) => $v->name, $values );
-
- $this->assertContains( 'RED', $names );
- $this->assertContains( 'GREEN', $names );
- $this->assertContains( 'BLUE', $names );
- }
-
- /**
- * @testdox an enum value's PHP value equals the underlying enum case (not just the backing scalar).
- */
- public function test_enum_value_is_php_enum_case(): void {
- $this->assertSame( ColorEnum::Red, ColorEnumType::get()->getValue( 'RED' )->value );
- $this->assertSame( ColorEnum::Blue, ColorEnumType::get()->getValue( 'BLUE' )->value );
- $this->assertSame( PriorityEnum::High, \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Priority::get()->getValue( 'HIGH' )->value );
- }
-
- /**
- * @testdox an interface trait becomes an InterfaceType with its declared fields.
- */
- public function test_interface_emits_fields_from_trait_properties(): void {
- $identifiable = IdentifiableInterfaceType::get();
- // Identifiable carries #[Name('HasId')] so its GraphQL name is renamed.
- $this->assertSame( 'HasId', $identifiable->name );
-
- $id_field = $identifiable->getField( 'id' );
- $this->assertSame( 'The unique numeric identifier', $id_field->description );
- }
-
- /**
- * @testdox interfaces compose: Named uses Identifiable, so Named exposes both fields.
- */
- public function test_interface_composition_includes_inherited_fields(): void {
- $named = NamedInterfaceType::get();
- $this->assertNotNull( $named->getField( 'id' ) );
- $this->assertNotNull( $named->getField( 'label' ) );
- }
-
- /**
- * @testdox an output type implements the interfaces of its trait stack.
- */
- public function test_output_types_implement_their_trait_interfaces(): void {
- $widget_iface_names = array_map( static fn( $i ) => $i->name, WidgetType::get()->getInterfaces() );
- $this->assertContains( 'Named', $widget_iface_names );
-
- $gadget_iface_names = array_map( static fn( $i ) => $i->name, GadgetType::get()->getInterfaces() );
- $this->assertContains( 'Named', $gadget_iface_names );
- }
-
- /**
- * @testdox the interface resolveType callback maps PHP classes to GraphQL types.
- */
- public function test_interface_resolve_type_dispatches_correctly(): void {
- $widget_query = $this->execute_query(
- '{ namedThing(kind: "widget") { __typename id } }'
- );
- $gadget_query = $this->execute_query(
- '{ namedThing(kind: "gadget") { __typename id } }'
- );
-
- $this->assertSame( 'Widget', $widget_query['data']['namedThing']['__typename'] ?? null );
- $this->assertSame( 'GadgetType', $gadget_query['data']['namedThing']['__typename'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/FieldAuthorizeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/FieldAuthorizeTest.php
deleted file mode 100644
index 46a03c7fa96..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/FieldAuthorizeTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-/**
- * End-to-end coverage for per-field authorization gates emitted into the
- * generated ObjectType resolvers.
- *
- * Fixture surface:
- *
- * - `Widget::$caption` carries `#[RequiredCapability('manage_woocommerce')]`,
- * so reading this field is gated by the WP `manage_woocommerce` capability.
- * - `Widget::$tag_ids` carries `#[PublicAccess]`, which at field level emits
- * a build warning and is treated as a no-op (allow-by-default). The
- * generated resolver therefore has no gate on `tag_ids`.
- *
- * Field-level denies surface as a per-field error in the GraphQL response;
- * the field value becomes `null` and the rest of the type resolves normally.
- */
-class FieldAuthorizeTest extends AutogeneratedTestCase {
- /**
- * @testdox An admin can read a gated field — the gate grants and the value is returned.
- */
- public function test_admin_can_read_gated_field(): void {
- $admin = self::factory()->user->create_and_get( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin->ID );
-
- $result = $this->execute_query( '{ publicWidget { caption } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result, 'Admin should not see any errors when reading a gated field.' );
- $this->assertArrayHasKey( 'data', $result );
- $this->assertArrayHasKey( 'publicWidget', $result['data'] );
- // Caption is nullable in the fixture; admin still gets through the gate.
- $this->assertArrayHasKey( 'caption', $result['data']['publicWidget'] );
- }
-
- /**
- * @testdox An anonymous principal hits a field-level deny when reading a gated field.
- */
- public function test_anonymous_is_denied_on_gated_field(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ publicWidget { caption } }' );
-
- $this->assertArrayHasKey( 'errors', $result, 'Anonymous reader should hit the field-level gate.' );
- $this->assertNotEmpty( $result['errors'] );
- $this->assertArrayHasKey( 'extensions', $result['errors'][0] );
- $this->assertSame( 'UNAUTHORIZED', $result['errors'][0]['extensions']['code'] );
-
- // The field value is null but other fields on the same type resolve normally.
- $this->assertArrayHasKey( 'data', $result );
- $this->assertNull( $result['data']['publicWidget']['caption'] );
- }
-
- /**
- * @testdox An authenticated principal without the required capability hits a FORBIDDEN deny.
- */
- public function test_subscriber_is_forbidden_on_gated_field(): void {
- $subscriber = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) );
- wp_set_current_user( $subscriber->ID );
-
- $result = $this->execute_query( '{ publicWidget { caption } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'FORBIDDEN', $result['errors'][0]['extensions']['code'] );
- $this->assertNull( $result['data']['publicWidget']['caption'] );
- }
-
- /**
- * @testdox A field-level deny carries a structured subject payload identifying type, field, and attribute.
- */
- public function test_field_deny_carries_subject_payload(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ publicWidget { caption } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertArrayHasKey( 'extensions', $result['errors'][0] );
- $this->assertArrayHasKey( 'subject', $result['errors'][0]['extensions'] );
-
- $subject = $result['errors'][0]['extensions']['subject'];
- $this->assertSame( 'Widget', $subject['type'] );
- $this->assertSame( 'caption', $subject['field'] );
- $this->assertSame( 'RequiredCapability', $subject['attribute'] );
- }
-
- /**
- * @testdox A query that does not select the gated field is unaffected by its gate.
- */
- public function test_unselected_gated_field_does_not_fire_gate(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ publicWidget { id slug } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'alpha', $result['data']['publicWidget']['slug'] );
- }
-
- /**
- * @testdox A field decorated with #[PublicAccess] alone has no gate (warned and ignored at build).
- */
- public function test_public_access_property_has_no_gate(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ publicWidget { tag_ids } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result, 'PublicAccess at field level is a build-only marker; no runtime gate should fire.' );
- $this->assertSame( array( 1, 2, 3 ), $result['data']['publicWidget']['tag_ids'] );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MetadataQueryHidingTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MetadataQueryHidingTest.php
deleted file mode 100644
index 1dc50f33792..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MetadataQueryHidingTest.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Api\Utils\SchemaHandle;
-
-/**
- * End-to-end coverage for the per-target `_apiMetadata` opt-out:
- *
- * - `Widget::$caption` carries `#[VisibleSampleMetadata]` and no
- * opt-out attribute. The generated schema preserves the entry; a
- * `_apiMetadata` row appears for the field.
- * - `Widget::$legacy_price` carries `#[HiddenFromMetadataQuery]` (the
- * stock marker). Any attribute on a target whose
- * `shows_in_metadata_query()` returns false hides the whole target —
- * the field does not appear in `_apiMetadata` at all, regardless of
- * what other metadata / authorization is declared on it.
- */
-class MetadataQueryHidingTest extends AutogeneratedTestCase {
- /**
- * @testdox Metadata attached to a visible target reaches `_apiMetadata`.
- */
- public function test_visible_metadata_appears_in_schema(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( type: 'Widget', field: 'caption' );
-
- $this->assertCount( 1, $rows, 'Expected exactly one metadata row for Widget.caption.' );
- $this->assertArrayHasKey( 'visible_sample', $rows[0]['entries'] );
- $this->assertSame( 'visible', $rows[0]['entries']['visible_sample'] );
- }
-
- /**
- * @testdox A target carrying any attribute whose shows_in_metadata_query() returns false is omitted entirely.
- */
- public function test_target_with_hidden_attribute_is_omitted(): void {
- $handle = new SchemaHandle( $this->build_schema() );
- $rows = $handle->find_metadata( type: 'Widget', field: 'legacy_price' );
-
- $this->assertSame(
- array(),
- $rows,
- 'legacy_price carries #[HiddenFromMetadataQuery] and must not surface through _apiMetadata.'
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationExecutionTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationExecutionTest.php
deleted file mode 100644
index 11fbf815436..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationExecutionTest.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-
-/**
- * Tests that the generated mutation resolvers — input-type conversion,
- * scalar-return wrapping, and capability enforcement — behave end-to-end.
- */
-class MutationExecutionTest extends AutogeneratedTestCase {
- /**
- * @testdox createWidget converts the input payload to a PHP object and persists.
- */
- public function test_create_widget_persists_through_input_conversion(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $query = 'mutation Create($input: CreateWidgetInput!) { createWidget(input: $input) { id label color tag_ids } }';
- $vars = array(
- 'input' => array(
- 'label' => 'Gamma',
- 'color' => 'GREEN',
- 'tag_ids' => array( 7, 8 ),
- ),
- );
-
- $result = $this->execute_query( $query, $vars );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $created = $result['data']['createWidget'] ?? null;
- $this->assertIsArray( $created );
- $this->assertSame( 'Gamma', $created['label'] );
- $this->assertSame( 'GREEN', $created['color'] );
- $this->assertSame( array( 7, 8 ), $created['tag_ids'] );
- }
-
- /**
- * @testdox createWidget rejects unauthenticated callers with UNAUTHORIZED.
- */
- public function test_create_widget_rejects_unauthenticated_callers(): void {
- wp_set_current_user( 0 );
-
- $query = 'mutation Create($input: CreateWidgetInput!) { createWidget(input: $input) { id } }';
- $vars = array(
- 'input' => array(
- 'label' => 'Should Not Persist',
- 'color' => 'RED',
- ),
- );
-
- $result = $this->execute_query( $query, $vars );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'UNAUTHORIZED', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox deleteWidget removes a widget from the store.
- */
- public function test_delete_widget_removes_from_store(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- DummyStore::seed();
- $this->assertNotNull( DummyStore::get_widget( 1 ) );
-
- $result = $this->execute_query( '{ widget(id: 1) { id } }' );
- $this->assertSame( 1, $result['data']['widget']['id'] ?? null );
-
- $mutation = 'mutation { deleteWidget(id: 1) { success message } }';
- $result = $this->execute_query( $mutation );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertTrue( $result['data']['deleteWidget']['success'] ?? false );
- $this->assertSame( 'Deleted widget 1.', $result['data']['deleteWidget']['message'] ?? null );
-
- // Re-querying returns null since the widget no longer exists.
- $followup = $this->execute_query( '{ widget(id: 1) { id } }' );
- $this->assertArrayHasKey( 'data', $followup );
- $this->assertArrayHasKey( 'widget', $followup['data'] );
- $this->assertNull( $followup['data']['widget'] );
- }
-
- /**
- * @testdox deleteWidget honours the `force` argument when the widget is missing.
- */
- public function test_delete_widget_honours_force_for_missing_widget(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $mutation = 'mutation { deleteWidget(id: 9999, force: true) { success message } }';
- $result = $this->execute_query( $mutation );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertTrue( $result['data']['deleteWidget']['success'] ?? false );
- }
-
- /**
- * @testdox a scalar-returning mutation wraps the result in a `result` field.
- */
- public function test_scalar_return_mutation_wraps_in_result(): void {
- $result = $this->execute_query( 'mutation { increment(value: 10, by: 5) { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 15, $result['data']['increment']['result'] ?? null );
- }
-
- /**
- * @testdox the scalar-return wrapper applies the parameter default when omitted.
- */
- public function test_scalar_return_mutation_uses_parameter_defaults(): void {
- $result = $this->execute_query( 'mutation { increment(value: 41) { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 42, $result['data']['increment']['result'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationInputAuthorizeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationInputAuthorizeTest.php
deleted file mode 100644
index c8a8db621ec..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/MutationInputAuthorizeTest.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-/**
- * End-to-end coverage for input-side authorization gates. Fixture surface:
- *
- * - `publicCreateWidget` is a `#[PublicAccess]` mutation that takes a
- * `CreateWidgetInput`. The class-level gate grants for any principal so
- * the test can exercise the input-side path in isolation.
- * - `CreateWidgetInput::$weight` carries `#[RequiredCapability('manage_woocommerce')]`.
- * The generated resolver emits a gate that runs only when `weight` is
- * actually present in the GraphQL request (mirroring
- * {@see \Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields}).
- */
-class MutationInputAuthorizeTest extends AutogeneratedTestCase {
- /**
- * @testdox An admin can create a widget while providing the gated input field.
- */
- public function test_admin_can_provide_gated_input_field(): void {
- $admin = self::factory()->user->create_and_get( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin->ID );
-
- $result = $this->execute_query(
- 'mutation { publicCreateWidget( input: { label: "Test", color: RED, weight: 100 } ) { slug } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertArrayHasKey( 'data', $result );
- $this->assertArrayHasKey( 'publicCreateWidget', $result['data'] );
- }
-
- /**
- * @testdox An anonymous caller can create a widget without providing the gated field.
- */
- public function test_anonymous_can_create_without_gated_field(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query(
- 'mutation { publicCreateWidget( input: { label: "TestAnon", color: GREEN } ) { slug } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $result, 'A non-provided gated input field should not trigger the gate.' );
- $this->assertArrayHasKey( 'data', $result );
- $this->assertArrayHasKey( 'publicCreateWidget', $result['data'] );
- }
-
- /**
- * @testdox An anonymous caller hits the input-side gate when providing the gated field.
- */
- public function test_anonymous_is_denied_when_providing_gated_input_field(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query(
- 'mutation { publicCreateWidget( input: { label: "TestDenied", color: GREEN, weight: 50 } ) { slug } }'
- );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'UNAUTHORIZED', $result['errors'][0]['extensions']['code'] );
- }
-
- /**
- * @testdox An input-side deny carries a structured subject payload identifying input type, field, and attribute.
- */
- public function test_input_deny_carries_subject_payload(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query(
- 'mutation { publicCreateWidget( input: { label: "TestSubject", color: GREEN, weight: 99 } ) { slug } }'
- );
-
- $this->assertArrayHasKey( 'errors', $result );
- $subject = $result['errors'][0]['extensions']['subject'];
- $this->assertSame( 'CreateWidgetInput', $subject['type'] );
- $this->assertSame( 'weight', $subject['field'] );
- $this->assertSame( 'RequiredCapability', $subject['attribute'] );
- }
-
- /**
- * @testdox A subscriber gets FORBIDDEN when providing a gated input field they lack the capability for.
- */
- public function test_subscriber_is_forbidden_when_providing_gated_input_field(): void {
- $subscriber = self::factory()->user->create_and_get( array( 'role' => 'subscriber' ) );
- wp_set_current_user( $subscriber->ID );
-
- $result = $this->execute_query(
- 'mutation { publicCreateWidget( input: { label: "TestSub", color: RED, weight: 75 } ) { slug } }'
- );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'FORBIDDEN', $result['errors'][0]['extensions']['code'] );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PaginationTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PaginationTest.php
deleted file mode 100644
index 8bf086d75f7..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PaginationTest.php
+++ /dev/null
@@ -1,125 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-
-/**
- * Tests for the pagination plumbing emitted around #[ConnectionOf]:
- * connection / edge types, cursor / page-info propagation, and the unrolled
- * PaginationParams / WidgetFilterInput parameters.
- */
-class PaginationTest extends AutogeneratedTestCase {
- /**
- * Run a `widgets` query as an administrator (the resolver requires both
- * manage_options and edit_posts).
- *
- * @param string $query Raw GraphQL query string.
- *
- * @return array{data?: ?array, errors?: array}
- */
- private function run_as_admin( string $query ): array {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- return $this->execute_query( $query );
- }
-
- /**
- * @testdox a connection field returns edges, nodes, page_info and total_count.
- */
- public function test_connection_returns_edges_nodes_pageinfo_total_count(): void {
- DummyStore::seed();
-
- $result = $this->run_as_admin(
- '{ widgets { edges { cursor node { id label } } nodes { id } page_info { has_next_page start_cursor end_cursor } total_count } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $widgets = $result['data']['widgets'] ?? null;
- $this->assertIsArray( $widgets );
-
- $this->assertArrayHasKey( 'edges', $widgets );
- $this->assertArrayHasKey( 'nodes', $widgets );
- $this->assertArrayHasKey( 'page_info', $widgets );
- $this->assertArrayHasKey( 'total_count', $widgets );
-
- $this->assertCount( 2, $widgets['edges'] );
- $this->assertSame( 1, $widgets['edges'][0]['node']['id'] ?? null );
- $this->assertSame( 'Alpha', $widgets['edges'][0]['node']['label'] ?? null );
- $this->assertSame( 2, $widgets['total_count'] );
- }
-
- /**
- * @testdox the unrolled WidgetFilterInput parameter exposes its public properties as args.
- */
- public function test_unrolled_filter_exposes_individual_args(): void {
- DummyStore::seed();
- DummyStore::create_widget( 'Crimson', Color::Red, 'crimson' );
-
- $result = $this->run_as_admin( '{ widgets(color: RED) { nodes { id label color } total_count } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $nodes = $result['data']['widgets']['nodes'] ?? null;
- $this->assertIsArray( $nodes );
- foreach ( $nodes as $node ) {
- $this->assertSame( 'RED', $node['color'] );
- }
- }
-
- /**
- * @testdox the search filter is applied through the unrolled input.
- */
- public function test_unrolled_filter_search_term(): void {
- DummyStore::seed();
-
- $result = $this->run_as_admin( '{ widgets(search: "alpha") { nodes { label } total_count } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $labels = array_column( $result['data']['widgets']['nodes'] ?? array(), 'label' );
- $this->assertContains( 'Alpha', $labels );
- $this->assertNotContains( 'Beta', $labels );
- }
-
- /**
- * @testdox PaginationParams arguments are unrolled at the schema level.
- */
- public function test_pagination_args_are_unrolled(): void {
- DummyStore::seed();
-
- $result = $this->run_as_admin( '{ widgets(first: 1) { nodes { id } total_count } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertCount( 1, $result['data']['widgets']['nodes'] ?? array() );
- $this->assertSame( 2, $result['data']['widgets']['total_count'] ?? null );
- }
-
- /**
- * @testdox a negative `first` value is rejected with INVALID_ARGUMENT.
- */
- public function test_negative_first_is_rejected(): void {
- DummyStore::seed();
-
- $result = $this->run_as_admin( '{ widgets(first: -1) { nodes { id } } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'INVALID_ARGUMENT', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a connection-typed property on an output type wires up the resolver.
- */
- public function test_connection_property_resolves_with_pagination_data(): void {
- DummyStore::seed();
-
- $result = $this->run_as_admin(
- '{ widget(id: 1) { id reviews { total_count nodes { id body } page_info { start_cursor } } } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 2, $result['data']['widget']['reviews']['total_count'] ?? null );
- $this->assertCount( 2, $result['data']['widget']['reviews']['nodes'] ?? array() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PrincipalParamTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PrincipalParamTest.php
deleted file mode 100644
index f867a3a726b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/PrincipalParamTest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-/**
- * Tests for the `_principal` infrastructure parameter on authorize() and execute().
- *
- * The principal flows in through the resolver `$context` populated by the
- * controller (or, in these tests, by {@see AutogeneratedTestCase::execute_query()}
- * mirroring the controller). The principal channel is non-nullable end-to-end:
- * anonymous requests are represented by a sentinel principal (a Principal
- * wrapping a WP_User with ID === 0), not by null.
- */
-class PrincipalParamTest extends AutogeneratedTestCase {
- /**
- * @testdox $_principal on execute() receives the resolved Principal carrying the authenticated user.
- */
- public function test_principal_aware_query_receives_authenticated_user(): void {
- $user_id = self::factory()->user->create(
- array(
- 'user_login' => 'alice',
- 'role' => 'subscriber',
- )
- );
- wp_set_current_user( $user_id );
-
- $result = $this->execute_query( '{ principalAware { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'alice', $result['data']['principalAware']['result'] ?? null );
- }
-
- /**
- * @testdox $_principal on execute() receives a sentinel anonymous principal for unauthenticated requests.
- */
- public function test_principal_aware_query_receives_anonymous_principal(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ principalAware { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'anonymous', $result['data']['principalAware']['result'] ?? null );
- }
-
- /**
- * @testdox the autogenerated resolver wires `_principal` into execute_args from $context.
- */
- public function test_resolver_source_wires_principal_from_context(): void {
- $source = (string) file_get_contents( __DIR__ . '/../Fixtures/DummyApiAutogenerated/GraphQLQueries/PrincipalAwareQuery.php' );
- $this->assertStringContainsString(
- "\$execute_args['_principal'] = \$context['principal'];",
- $source
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/QueryExecutionTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/QueryExecutionTest.php
deleted file mode 100644
index 0615d39f961..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/QueryExecutionTest.php
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\GetWidget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-
-/**
- * Tests that the generated query resolvers dispatch correctly against the
- * dummy fixture API and propagate values through to the GraphQL response.
- */
-class QueryExecutionTest extends AutogeneratedTestCase {
- /**
- * @testdox a public query without parameters returns the resolver's value.
- */
- public function test_public_query_returns_resolver_value(): void {
- $result = $this->execute_query( '{ greeting { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'Hello, world!', $result['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox a public query forwards optional arguments to the command.
- */
- public function test_public_query_forwards_arguments(): void {
- $result = $this->execute_query( '{ greeting(name: "Alice") { result } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 'Hello, Alice!', $result['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox a capability-protected query succeeds when the cap is held.
- */
- public function test_protected_query_succeeds_when_authorized(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
- DummyStore::seed();
-
- $result = $this->execute_query( '{ widget(id: 1) { id label } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 1, $result['data']['widget']['id'] ?? null );
- $this->assertSame( 'Alpha', $result['data']['widget']['label'] ?? null );
- }
-
- /**
- * @testdox the resolver routes through the configured Container.
- */
- public function test_resolver_routes_through_container(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $override = new class() extends GetWidget {
- /**
- * Always returns a sentinel widget.
- *
- * @param int $id The widget id (unused).
- */
- public function execute( int $id ): ?\Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget {
- unset( $id );
- $widget = new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget();
- $widget->id = 999;
- $widget->label = 'OVERRIDDEN';
- $widget->slug = 'overridden';
- $widget->caption = null;
- $widget->color = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color::Blue;
- $widget->priority = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority::Normal;
- $widget->tag_ids = array();
- $widget->featured_reviews = array();
- $reviews = new \Automattic\WooCommerce\Api\Pagination\Connection();
- $reviews->edges = array();
- $reviews->nodes = array();
- $reviews->page_info = new \Automattic\WooCommerce\Api\Pagination\PageInfo();
- $reviews->page_info->has_next_page = false;
- $reviews->page_info->has_previous_page = false;
- $reviews->page_info->start_cursor = null;
- $reviews->page_info->end_cursor = null;
- $reviews->total_count = 0;
- $widget->reviews = $reviews;
- $widget->date_created = null;
- $widget->price = '0';
- $widget->legacy_price = '0';
- return $widget;
- }
- };
- DummyContainer::set_instance( GetWidget::class, $override );
-
- $result = $this->execute_query( '{ widget(id: 1) { id label } }' );
-
- $this->assertArrayNotHasKey( 'errors', $result );
- $this->assertSame( 999, $result['data']['widget']['id'] ?? null );
- $this->assertSame( 'OVERRIDDEN', $result['data']['widget']['label'] ?? null );
- }
-
- /**
- * @testdox a query that throws InvalidArgumentException surfaces an INVALID_ARGUMENT error.
- */
- public function test_invalid_argument_exception_translates_to_invalid_argument_code(): void {
- $result = $this->execute_query( '{ failing(kind: "invalid_argument") { result } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'INVALID_ARGUMENT', $result['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'Bad input from caller.', $result['errors'][0]['message'] ?? null );
- }
-
- /**
- * @testdox a query that throws ApiException carries its custom code through.
- */
- public function test_api_exception_carries_custom_code(): void {
- $result = $this->execute_query( '{ failing(kind: "api_exception") { result } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'CUSTOM_FAILURE', $result['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'Custom failure.', $result['errors'][0]['message'] ?? null );
- $this->assertSame( 'extra', $result['errors'][0]['extensions']['detail'] ?? null );
- }
-
- /**
- * @testdox a query that throws an unexpected error is masked behind INTERNAL_ERROR.
- */
- public function test_unexpected_throwable_masks_internal_error(): void {
- $result = $this->execute_query( '{ failing(kind: "runtime") { result } }' );
-
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'INTERNAL_ERROR', $result['errors'][0]['extensions']['code'] ?? null );
- // The wrapping GraphQLError carries 'An unexpected error occurred.', but
- // because its `previous` is a non-ClientAware throwable webonyx replaces
- // the message with its generic 'Internal server error' on the wire.
- // Either is acceptable here — the contract is that the *resolver-side*
- // message does not leak.
- $this->assertNotEquals( 'leaky internals', $result['errors'][0]['message'] ?? null );
- }
-
- /**
- * @testdox a query returning an interface dispatches via resolveType.
- */
- public function test_interface_return_dispatches_to_concrete_type(): void {
- $widget_result = $this->execute_query(
- '{ namedThing(kind: "widget") { ... on Widget { id label slug } } }'
- );
- $gadget_result = $this->execute_query(
- '{ namedThing(kind: "gadget") { ... on GadgetType { id label parts_count } } }'
- );
-
- $this->assertArrayNotHasKey( 'errors', $widget_result );
- $this->assertSame( 'Alpha', $widget_result['data']['namedThing']['label'] ?? null );
- $this->assertSame( 'alpha', $widget_result['data']['namedThing']['slug'] ?? null );
-
- $this->assertArrayNotHasKey( 'errors', $gadget_result );
- $this->assertSame( 'Sample Gadget', $gadget_result['data']['namedThing']['label'] ?? null );
- $this->assertSame( 7, $gadget_result['data']['namedThing']['parts_count'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/RuntimeMetadataVisibilityTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/RuntimeMetadataVisibilityTest.php
deleted file mode 100644
index d609c94b608..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/RuntimeMetadataVisibilityTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Api\Utils\SchemaHandle;
-
-/**
- * Regression coverage for the `shows_in_metadata_query()` / `#[HiddenFromMetadataQuery]`
- * opt-out being *discovery-only*: a target hidden from `_apiMetadata` must still
- * feed its metadata into the runtime `$_metadata` slices that field gates read.
- *
- * `HiddenFlaggedQuery` (hidden, carries `runtime_flag`) returns `RuntimeMetaProbe`
- * (hidden type, carries `runtime_flag`), whose three gated fields each read a
- * different slice via `GrantsIfMetadataFlag`:
- *
- * - `by_type` → `$_metadata['type']` (the hidden type's metadata)
- * - `by_field` → `$_metadata['field']` (the hidden field's own metadata)
- * - `by_query` → `$_metadata['query']` (the hidden query's metadata)
- *
- * All three must grant. Before the runtime/discovery split, each slice was
- * blanked when its carrying target opted out, so every field would have denied.
- */
-class RuntimeMetadataVisibilityTest extends AutogeneratedTestCase {
- /**
- * @testdox runtime $_metadata slices stay populated for hidden targets (query / type / field).
- */
- public function test_hidden_targets_keep_runtime_metadata(): void {
- wp_set_current_user( 0 );
-
- $result = $this->execute_query( '{ hiddenFlagged { by_type by_field by_query } }' );
-
- $this->assertArrayNotHasKey(
- 'errors',
- $result,
- 'A discovery opt-out must not starve the runtime gates: ' . wp_json_encode( $result['errors'] ?? array() )
- );
- $this->assertSame( 'type-ok', $result['data']['hiddenFlagged']['by_type'], 'type slice was blanked at runtime' );
- $this->assertSame( 'field-ok', $result['data']['hiddenFlagged']['by_field'], 'field slice was blanked at runtime' );
- $this->assertSame( 'query-ok', $result['data']['hiddenFlagged']['by_query'], 'query slice was blanked at runtime' );
- }
-
- /**
- * @testdox the discovery opt-out still hides the target from _apiMetadata.
- */
- public function test_hidden_field_absent_from_discovery(): void {
- $handle = new SchemaHandle( $this->build_schema() );
-
- // by_field carries metadata + an authorization attribute but is hidden,
- // so it must not surface through _apiMetadata at all — confirming the
- // runtime fix did not re-expose hidden targets in discovery.
- $this->assertSame(
- array(),
- $handle->find_metadata( type: 'RuntimeMetaProbe', field: 'by_field' ),
- 'by_field carries #[HiddenFromMetadataQuery] and must stay out of _apiMetadata.'
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/SchemaShapeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/SchemaShapeTest.php
deleted file mode 100644
index ecf0115ac76..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Autogenerated/SchemaShapeTest.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Autogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLController as DummyGraphQLController;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorEnumType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Priority as PriorityEnumType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input\CreateWidget as CreateWidgetInputType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Identifiable as IdentifiableInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterfaceType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Gadget as GadgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination\WidgetConnection as WidgetConnectionType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Scalars\DummyDateTime as DummyDateTimeType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootMutationType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\RootQueryType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\TypeRegistry;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-/**
- * Top-level shape assertions on the autogenerated dummy schema. These verify
- * that the builder emits the expected named types in the expected categories
- * (object / input / interface / enum / scalar / connection / edge).
- */
-class SchemaShapeTest extends AutogeneratedTestCase {
- /**
- * @testdox the autogenerated GraphQLControllerBase extends the abstract base controller.
- */
- public function test_generated_controller_extends_base(): void {
- $this->assertTrue(
- is_subclass_of( DummyGraphQLController::class, \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase::class )
- );
- }
-
- /**
- * @testdox RootQueryType exposes every non-ignored query and skips ignored ones.
- */
- public function test_root_query_lists_every_non_ignored_query(): void {
- $root_query = RootQueryType::get();
-
- $this->assertInstanceOf( ObjectType::class, $root_query );
- $this->assertSame( 'Query', $root_query->name );
-
- $fields = $root_query->getFields();
- $this->assertArrayHasKey( 'widget', $fields );
- $this->assertArrayHasKey( 'widgets', $fields );
- $this->assertArrayHasKey( 'widgetList', $fields );
- $this->assertArrayHasKey( 'greeting', $fields );
- $this->assertArrayHasKey( 'namedThing', $fields );
- $this->assertArrayHasKey( 'failing', $fields );
- // IgnoredQuery has #[Ignore] and must not show up.
- $this->assertArrayNotHasKey( 'ignoredQuery', $fields );
- $this->assertArrayNotHasKey( 'IgnoredQuery', $fields );
- }
-
- /**
- * @testdox a query returning #[ArrayOf( X::class )] array emits a [X!]! list field.
- */
- public function test_array_of_return_type_emits_non_null_list(): void {
- // ListWidgetsArray::execute() is declared `: array` with
- // `#[ArrayOf( Widget::class )]`. The builder must turn that into a
- // non-null list of non-null Widget — not the scalar fallback. Regression
- // guard for the return-type ArrayOf path (#[ConnectionOf] is covered by
- // the `widgets` field separately).
- $field = RootQueryType::get()->getFields()['widgetList'];
- $this->assertSame( '[Widget!]!', (string) $field->getType() );
- }
-
- /**
- * @testdox RootMutationType exposes every mutation and uses the right field names.
- */
- public function test_root_mutation_lists_every_mutation(): void {
- $root_mutation = RootMutationType::get();
-
- $this->assertSame( 'Mutation', $root_mutation->name );
-
- $fields = $root_mutation->getFields();
- $this->assertArrayHasKey( 'createWidget', $fields );
- $this->assertArrayHasKey( 'deleteWidget', $fields );
- $this->assertArrayHasKey( 'increment', $fields );
- }
-
- /**
- * @testdox TypeRegistry returns every concrete output type that implements an interface.
- */
- public function test_type_registry_returns_implementors(): void {
- $types = TypeRegistry::get_interface_implementors();
- $names = array_map(
- static fn( $t ) => $t->name,
- $types
- );
-
- $this->assertContains( 'Widget', $names );
- $this->assertContains( 'GadgetType', $names );
- $this->assertContains( 'WidgetReview', $names );
- }
-
- /**
- * @testdox each generated category emits a singleton of the right type.
- */
- public function test_generated_categories_have_correct_runtime_types(): void {
- $this->assertInstanceOf( ObjectType::class, WidgetType::get() );
- $this->assertInstanceOf( ObjectType::class, GadgetType::get() );
- $this->assertInstanceOf( ObjectType::class, WidgetConnectionType::get() );
- $this->assertInstanceOf( EnumType::class, ColorEnumType::get() );
- $this->assertInstanceOf( EnumType::class, PriorityEnumType::get() );
- $this->assertInstanceOf( InterfaceType::class, IdentifiableInterfaceType::get() );
- $this->assertInstanceOf( InterfaceType::class, NamedInterfaceType::get() );
- $this->assertInstanceOf( InputObjectType::class, CreateWidgetInputType::get() );
- $this->assertInstanceOf( CustomScalarType::class, DummyDateTimeType::get() );
- }
-
- /**
- * @testdox the generated singletons are stable — get() returns the same instance every call.
- */
- public function test_generated_singletons_are_stable(): void {
- $this->assertSame( WidgetType::get(), WidgetType::get() );
- $this->assertSame( ColorEnumType::get(), ColorEnumType::get() );
- $this->assertSame( DummyDateTimeType::get(), DummyDateTimeType::get() );
- }
-
- /**
- * @testdox the dummy schema introspects without errors.
- */
- public function test_schema_assertion_passes(): void {
- // Schema::assertValid() throws on internal inconsistencies (missing
- // implementors, mismatched interface fields, etc.). If anything in
- // the generated tree is malformed this throws.
- $this->build_schema()->assertValid();
- $this->assertTrue( true );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/ComputePreauthorizedTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/ComputePreauthorizedTest.php
deleted file mode 100644
index ef6b4a0d81d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/ComputePreauthorizedTest.php
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\ComposedAuthorizeQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\MetadataAwareInternalQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\MetadataAwareNoFlagQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\InheritedCapQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\InheritedPublicQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\MergedCapsQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\ComposedAuthorizeQuery as ComposedAuthorizeResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\InheritedCapQuery as InheritedCapResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\InheritedPublicQuery as InheritedPublicResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MergedCapsQuery as MergedCapsResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MetadataAwareInternalQuery as MetadataAwareInternalResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MetadataAwareNoFlagQuery as MetadataAwareNoFlagResolver;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the dual `compute_preauthorized` paths:
- *
- * 1. The static helper emitted on each autogenerated resolver.
- * 2. The runtime `ResolverHelpers::compute_preauthorized()` helper that walks the
- * command class's attributes via Reflection.
- *
- * Both paths must agree on every command — they implement the same
- * AND-of-attributes semantics, just at different stages of the build.
- *
- * Anonymous principals are represented by a {@see Principal} wrapping a
- * `WP_User` with `ID === 0`, mirroring what the controller passes through
- * the resolver context.
- */
-class ComputePreauthorizedTest extends WC_Unit_Test_Case {
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- wp_set_current_user( 0 );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- wp_set_current_user( 0 );
- parent::tearDown();
- }
-
- /**
- * Build a {@see Principal} for the given role, or anonymous when null.
- *
- * @param ?string $role A WP role slug, or null to construct an anonymous principal.
- */
- private function principal_with_role( ?string $role ): Principal {
- if ( null === $role ) {
- return new Principal( new \WP_User() );
- }
- $user = self::factory()->user->create_and_get( array( 'role' => $role ) );
- return new Principal( $user );
- }
-
- /**
- * @testdox the autogen-emitted helper returns true for an admin on a single-cap command.
- */
- public function test_autogen_helper_grants_for_admin_on_inherited_cap(): void {
- $admin = $this->principal_with_role( 'administrator' );
- $rejected = $this->principal_with_role( 'subscriber' );
- $anonymous = $this->principal_with_role( null );
-
- $this->assertTrue( InheritedCapResolver::compute_preauthorized( $admin ) );
- $this->assertFalse( InheritedCapResolver::compute_preauthorized( $rejected ) );
- $this->assertFalse( InheritedCapResolver::compute_preauthorized( $anonymous ) );
- }
-
- /**
- * @testdox the autogen-emitted helper short-circuits to true for PublicAccess.
- */
- public function test_autogen_helper_returns_true_for_public_access(): void {
- $this->assertTrue( InheritedPublicResolver::compute_preauthorized( $this->principal_with_role( null ) ) );
- $this->assertTrue( InheritedPublicResolver::compute_preauthorized( $this->principal_with_role( 'administrator' ) ) );
- }
-
- /**
- * @testdox the autogen-emitted helper ANDs multiple inherited capabilities.
- */
- public function test_autogen_helper_and_merged_caps(): void {
- // The MergedCapsQuery requires manage_options AND edit_posts.
- // `editor` has edit_posts but not manage_options.
- $admin = $this->principal_with_role( 'administrator' );
- $editor = $this->principal_with_role( 'editor' );
- $subscriber = $this->principal_with_role( 'subscriber' );
-
- $this->assertTrue( MergedCapsResolver::compute_preauthorized( $admin ) );
- $this->assertFalse( MergedCapsResolver::compute_preauthorized( $editor ) );
- $this->assertFalse( MergedCapsResolver::compute_preauthorized( $subscriber ) );
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized agrees with the autogen helper for inherited capabilities.
- */
- public function test_utils_helper_matches_autogen_for_inherited_cap(): void {
- foreach ( array( 'administrator', 'subscriber', null ) as $role ) {
- $principal = $this->principal_with_role( $role );
- $autogen = InheritedCapResolver::compute_preauthorized( $principal );
- $utils = ResolverHelpers::compute_preauthorized( InheritedCapQuery::class, $principal );
- $this->assertSame(
- $autogen,
- $utils,
- 'Drift between autogen and ResolverHelpers for InheritedCapQuery, role: ' . ( $role ?? '<anonymous>' )
- );
- }
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized agrees with the autogen helper for PublicAccess.
- */
- public function test_utils_helper_matches_autogen_for_public_access(): void {
- $anonymous = $this->principal_with_role( null );
- $this->assertTrue( ResolverHelpers::compute_preauthorized( InheritedPublicQuery::class, $anonymous ) );
- $this->assertSame(
- InheritedPublicResolver::compute_preauthorized( $anonymous ),
- ResolverHelpers::compute_preauthorized( InheritedPublicQuery::class, $anonymous )
- );
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized agrees with the autogen helper for merged caps.
- */
- public function test_utils_helper_matches_autogen_for_merged_caps(): void {
- foreach ( array( 'administrator', 'editor', null ) as $role ) {
- $principal = $this->principal_with_role( $role );
- $this->assertSame(
- MergedCapsResolver::compute_preauthorized( $principal ),
- ResolverHelpers::compute_preauthorized( MergedCapsQuery::class, $principal )
- );
- }
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized works on a command with both attributes and authorize().
- */
- public function test_utils_helper_for_command_with_attribute_and_authorize_method(): void {
- // ComposedAuthorizeQuery has both an attribute AND an authorize() method.
- // compute_preauthorized() answers the attribute-level question only.
- $admin = $this->principal_with_role( 'administrator' );
-
- $this->assertTrue( ComposedAuthorizeResolver::compute_preauthorized( $admin ) );
- $this->assertSame(
- ComposedAuthorizeResolver::compute_preauthorized( $admin ),
- ResolverHelpers::compute_preauthorized( ComposedAuthorizeQuery::class, $admin )
- );
-
- $anonymous = $this->principal_with_role( null );
- $this->assertFalse( ComposedAuthorizeResolver::compute_preauthorized( $anonymous ) );
- $this->assertSame(
- ComposedAuthorizeResolver::compute_preauthorized( $anonymous ),
- ResolverHelpers::compute_preauthorized( ComposedAuthorizeQuery::class, $anonymous )
- );
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized rejects an unknown class.
- */
- public function test_utils_helper_rejects_unknown_class(): void {
- $this->expectException( \InvalidArgumentException::class );
- ResolverHelpers::compute_preauthorized( '\\Definitely\\Not\\A\\Class', $this->principal_with_role( null ) );
- }
-
- /**
- * @testdox the autogen-emitted helper threads the class's #[Metadata] entries into $_metadata.
- */
- public function test_autogen_helper_threads_metadata_slot(): void {
- $principal = $this->principal_with_role( 'administrator' );
-
- // The RequiresInternalFlag attribute grants only when $_metadata['query']['internal'] === true.
- $this->assertTrue(
- MetadataAwareInternalResolver::compute_preauthorized( $principal ),
- '#[Internal] metadata should reach the attribute via $_metadata.'
- );
- $this->assertFalse(
- MetadataAwareNoFlagResolver::compute_preauthorized( $principal ),
- 'A command with no #[Internal] should produce an empty `query` slice and deny.'
- );
- }
-
- /**
- * @testdox ResolverHelpers::compute_preauthorized threads $_metadata to agree with the autogen helper.
- */
- public function test_utils_helper_threads_metadata_slot(): void {
- $principal = $this->principal_with_role( 'administrator' );
-
- $this->assertSame(
- MetadataAwareInternalResolver::compute_preauthorized( $principal ),
- ResolverHelpers::compute_preauthorized( MetadataAwareInternalQuery::class, $principal )
- );
- $this->assertSame(
- MetadataAwareNoFlagResolver::compute_preauthorized( $principal ),
- ResolverHelpers::compute_preauthorized( MetadataAwareNoFlagQuery::class, $principal )
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/CountingNodeList.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/CountingNodeList.php
deleted file mode 100644
index 0be920be345..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/CountingNodeList.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures;
-
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-
-/**
- * NodeList that counts how many times it is iterated.
- *
- * Tests swap it in for the `selections` of fragment selection sets to assert
- * how many times a walk over the document visits each fragment, however often
- * the fragment is spread.
- */
-final class CountingNodeList extends NodeList {
- /**
- * Number of iterations started over any CountingNodeList since the last reset().
- *
- * @var int
- */
- public static int $iterations = 0;
-
- /**
- * Reset the iteration counter.
- */
- public static function reset(): void {
- self::$iterations = 0;
- }
-
- /**
- * Replace the selections of a selection set with a counting copy.
- *
- * @param SelectionSetNode $selection_set The selection set to instrument.
- */
- public static function instrument( SelectionSetNode $selection_set ): void {
- $selection_set->selections = new self( iterator_to_array( $selection_set->selections ) );
- }
-
- /**
- * Instrument the selection set of every named fragment in a document.
- *
- * @param DocumentNode $document The parsed document.
- * @return int The number of fragments instrumented.
- */
- public static function instrument_fragments( DocumentNode $document ): int {
- $count = 0;
- foreach ( $document->definitions as $definition ) {
- if ( $definition instanceof FragmentDefinitionNode ) {
- self::instrument( $definition->selectionSet );
- ++$count;
- }
- }
- return $count;
- }
-
- /**
- * Count the iteration, then iterate as usual.
- */
- public function getIterator(): \Traversable {
- ++self::$iterations;
- return parent::getIterator();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/GrantsIfMetadataFlag.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/GrantsIfMetadataFlag.php
deleted file mode 100644
index 5b53d8f6cb7..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/GrantsIfMetadataFlag.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes;
-
-use Attribute;
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-
-/**
- * Fixture authorization attribute that grants iff a `runtime_flag` metadata
- * entry (value `true`) is present in the requested `$_metadata` slice
- * (`query`, `type`, or `field`).
- *
- * Used to prove that the runtime `$_metadata` slices stay populated even when
- * the target carrying the metadata opts out of `_apiMetadata` via
- * `#[HiddenFromMetadataQuery]` — that opt-out is discovery-only and must not
- * starve the runtime gate.
- */
-#[Attribute( Attribute::TARGET_PROPERTY )]
-final class GrantsIfMetadataFlag {
- /**
- * @param string $slice Which `$_metadata` slice to read: `query`, `type`, or `field`.
- */
- public function __construct( public readonly string $slice ) {
- }
-
- /**
- * @param Principal $principal The resolved principal (unused; the decision is metadata-driven).
- * @param array $_metadata The harvested metadata slices.
- */
- public function authorize( Principal $principal, array $_metadata ): bool {
- unset( $principal );
- return true === ( $_metadata[ $this->slice ]['runtime_flag'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/RequiresInternalFlag.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/RequiresInternalFlag.php
deleted file mode 100644
index f1971645637..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Attributes/RequiresInternalFlag.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes;
-
-use Attribute;
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-
-/**
- * Fixture authorization attribute that exercises the opt-in `$_metadata`
- * slot. Grants iff the surrounding command class carries an `#[Internal]`
- * metadata entry (i.e. `$_metadata['query']['internal'] === true`).
- *
- * Declaring both the principal and `$_metadata` covers the
- * happy-path mixed-positional/named call shape ApiBuilder and
- * {@see \Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers::compute_preauthorized()}
- * must produce.
- */
-#[Attribute( Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY )]
-final class RequiresInternalFlag {
- public function authorize( Principal $principal, array $_metadata ): bool {
- unset( $principal );
- return true === ( $_metadata['query']['internal'] ?? null );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/PublicAccessTrait.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/PublicAccessTrait.php
deleted file mode 100644
index 4c4abbac74f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/PublicAccessTrait.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-
-/**
- * Trait carrying #[PublicAccess]; queries that `use` it inherit public
- * access without having to declare the attribute themselves.
- */
-#[PublicAccess]
-trait PublicAccessTrait {
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresEditPostsTrait.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresEditPostsTrait.php
deleted file mode 100644
index b09076f78d3..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresEditPostsTrait.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Trait carrying #[RequiredCapability('edit_posts')]. Combined with another
- * inheritance source (parent class) on the same query class, the builder
- * should merge the capabilities from both into the generated check list.
- */
-#[RequiredCapability( 'edit_posts' )]
-trait RequiresEditPostsTrait {
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresManageOptions.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresManageOptions.php
deleted file mode 100644
index d4218a509c8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Authorization/RequiresManageOptions.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * PHP interface that carries a #[RequiredCapability] attribute. Used to
- * verify that ApiBuilder honours capability inheritance via implements clauses
- * (in addition to parent classes and traits).
- *
- * Lives in the non-classified Authorization/ directory so the builder skips
- * it during discovery — it's a helper, not a code-API concept itself.
- */
-#[RequiredCapability( 'manage_options' )]
-interface RequiresManageOptions {
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Color.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Color.php
deleted file mode 100644
index 2421f25439f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Color.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'A simple color palette' )]
-enum Color: string {
- #[Description( 'Red' )]
- case Red = 'red';
-
- #[Description( 'Green' )]
- case Green = 'green';
-
- case Blue = 'blue';
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Priority.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Priority.php
deleted file mode 100644
index dd13375176b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Enums/Priority.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums;
-
-use Automattic\WooCommerce\Api\Attributes\Deprecated;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-
-/**
- * Exercises class-level #[Name] (renames the GraphQL type) and case-level
- * #[Name] / #[Deprecated] / #[Description].
- */
-#[Name( 'TaskPriority' )]
-#[Description( 'Priority level for a task' )]
-enum Priority: string {
- #[Description( 'Low priority' )]
- case Low = 'low';
-
- #[Name( 'NORMAL_PRIORITY' )]
- case Normal = 'normal';
-
- #[Description( 'High priority' )]
- #[Deprecated( 'Use NORMAL_PRIORITY instead.' )]
- case High = 'high';
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Infrastructure/ClassResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Infrastructure/ClassResolver.php
deleted file mode 100644
index 5afa47cc407..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Infrastructure/ClassResolver.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure;
-
-/**
- * Class resolver for the dummy code-API used by the GraphQL infrastructure
- * tests.
- *
- * Mirrors the public signature ApiBuilder requires: a public static
- * `resolve_class(string): object` method. Tests can swap the underlying instances via
- * {@see self::set_instance()} so a single resolver dispatch can be observed
- * with a known command instance.
- */
-final class ClassResolver {
- /**
- * @var array<class-string, object>
- */
- private static array $instances = array();
-
- public static function set_instance( string $class_name, object $instance ): void {
- self::$instances[ $class_name ] = $instance;
- }
-
- public static function reset(): void {
- self::$instances = array();
- }
-
- public static function resolve_class( string $class_name ): object {
- if ( isset( self::$instances[ $class_name ] ) ) {
- return self::$instances[ $class_name ];
- }
- return new $class_name();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/CreateWidgetInput.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/CreateWidgetInput.php
deleted file mode 100644
index d83aeca82a2..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/CreateWidgetInput.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\InputTypes\TracksProvidedFields;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Scalars\DummyDateTime;
-
-/**
- * Input type for creating a widget.
- */
-#[Description( 'Data needed to create a new widget' )]
-class CreateWidgetInput {
- use TracksProvidedFields;
-
- #[Description( 'The widget label' )]
- public string $label;
-
- #[Description( 'Optional weight in grams' )]
- #[RequiredCapability( 'manage_woocommerce' )]
- public ?int $weight = null;
-
- #[Description( 'The widget color' )]
- public Color $color;
-
- #[Description( 'Tag IDs to attach to the widget' )]
- #[ArrayOf( 'int' )]
- public ?array $tag_ids = null;
-
- #[Description( 'When the widget should expire' )]
- #[ScalarType( DummyDateTime::class )]
- public ?string $expires_at = null;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/WidgetFilterInput.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/WidgetFilterInput.php
deleted file mode 100644
index 02debb0c0ed..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/InputTypes/WidgetFilterInput.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color;
-
-/**
- * Filter input applied to widget listings. Used as an unrolled #[Unroll]
- * parameter on `ListWidgets::execute()` so its public properties become
- * individual GraphQL arguments.
- *
- * Carries an explicit constructor with promoted parameters because the
- * generator emits `new WidgetFilterInput(search: ..., color: ...)` for the
- * unrolled call site.
- */
-#[Name( 'WidgetFilterArgs' )]
-#[Description( 'Filters applied to a widget listing' )]
-class WidgetFilterInput {
- /**
- * Constructor.
- *
- * @param ?string $search A free-text search term.
- * @param ?Color $color Filter widgets by color.
- */
- public function __construct(
- #[Description( 'A free-text search term' )]
- public ?string $search = null,
- #[Description( 'Filter widgets by color' )]
- public ?Color $color = null,
- ) {
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Identifiable.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Identifiable.php
deleted file mode 100644
index 77da49008b8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Identifiable.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-
-/**
- * Interface trait exposing a numeric identifier.
- *
- * Carries a class-level #[Name] override so the GraphQL interface name is
- * `HasId`. Pairs with the un-renamed {@see Named} trait so both branches of
- * the interface-name code path are covered.
- */
-#[Name( 'HasId' )]
-#[Description( 'An object with a numeric identifier' )]
-trait Identifiable {
- #[Description( 'The unique numeric identifier' )]
- public int $id;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Named.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Named.php
deleted file mode 100644
index 7ab61b19d31..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Interfaces/Named.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Interface trait that gives a type a human-readable label.
- */
-#[Description( 'An object with a human-readable label' )]
-trait Named {
- use Identifiable;
-
- #[Description( 'The display label for this object' )]
- public string $label;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Metadata/VisibleSampleMetadata.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Metadata/VisibleSampleMetadata.php
deleted file mode 100644
index 305c48f9144..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Metadata/VisibleSampleMetadata.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Metadata;
-
-use Attribute;
-use Automattic\WooCommerce\Api\Attributes\Metadata;
-
-/**
- * Plain fixture metadata subclass with the default
- * `shows_in_metadata_query()` (returns `true`). Provides a known entry
- * name (`visible_sample`) for tests that assert on the metadata query's
- * output without bringing the side effects of stock subclasses like
- * `#[Internal]` (which prefixes descriptions).
- */
-#[Attribute( Attribute::TARGET_PROPERTY )]
-final class VisibleSampleMetadata extends Metadata {
- public function __construct() {
- parent::__construct( 'visible_sample', 'visible' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/CreateWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/CreateWidget.php
deleted file mode 100644
index 60d86666204..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/CreateWidget.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-/**
- * Creates a widget — exercises:
- * - input type to PHP class conversion in the generated resolver.
- * - object return type.
- * - #[RequiredCapability] enforcement.
- */
-#[Description( 'Create a new widget' )]
-#[RequiredCapability( 'manage_options' )]
-class CreateWidget {
- public function execute(
- #[Description( 'The data for the new widget' )]
- CreateWidgetInput $input,
- #[Description( 'Related widget inputs for array input generation coverage' )]
- #[ArrayOf( CreateWidgetInput::class )]
- ?array $related_inputs = null,
- ): Widget {
- $widget = Store::create_widget( $input->label, $input->color );
- if ( null !== $input->weight ) {
- $widget->caption = sprintf( 'weighs %d g', $input->weight );
- }
- if ( $input->was_provided( 'tag_ids' ) && null !== $input->tag_ids ) {
- $widget->tag_ids = $input->tag_ids;
- }
- return $widget;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/DeleteWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/DeleteWidget.php
deleted file mode 100644
index c105d24d897..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/DeleteWidget.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\OperationResult;
-
-#[Description( 'Delete a widget' )]
-#[RequiredCapability( 'manage_options' )]
-class DeleteWidget {
- public function execute(
- #[Description( 'The widget id to delete' )]
- int $id,
- #[Description( 'When true, ignore "not found" errors' )]
- bool $force = false,
- ): OperationResult {
- $result = new OperationResult();
- if ( Store::delete_widget( $id ) ) {
- $result->success = true;
- $result->message = sprintf( 'Deleted widget %d.', $id );
- return $result;
- }
- $result->success = $force;
- $result->message = $force ? 'Widget not found, force was set.' : 'Widget not found.';
- return $result;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/Increment.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/Increment.php
deleted file mode 100644
index 488b8a8542f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/Increment.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-
-/**
- * Mutation that returns a scalar (int) — exercises the generator's "wrap a
- * scalar return in a result object" path on the mutation side.
- */
-#[Name( 'increment' )]
-#[Description( 'Increment a value by an optional amount' )]
-#[PublicAccess]
-class Increment {
- public function execute(
- #[Description( 'The starting value' )]
- int $value,
- #[Description( 'How much to add' )]
- int $by = 1,
- ): int {
- return $value + $by;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/PublicCreateWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/PublicCreateWidget.php
deleted file mode 100644
index 953941b90bd..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Mutations/PublicCreateWidget.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-/**
- * Publicly-accessible widget creator used by input-level authorization tests.
- * Anyone can invoke the mutation (no class-level gate), but the
- * `CreateWidgetInput::$weight` property carries a `#[RequiredCapability]`, so
- * the input-side gate fires only when `weight` is provided in the request.
- */
-#[Name( 'publicCreateWidget' )]
-#[Description( 'Create a widget without query-level gating; input-side gates apply.' )]
-#[PublicAccess]
-class PublicCreateWidget {
- public function execute( CreateWidgetInput $input ): Widget {
- $widget = Store::create_widget( $input->label, $input->color );
- if ( null !== $input->weight ) {
- $widget->caption = sprintf( 'weighs %d g', $input->weight );
- }
- return $widget;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeOnlyQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeOnlyQuery.php
deleted file mode 100644
index 8949e9d4411..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeOnlyQuery.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-
-/**
- * No #[RequiredCapability] / #[PublicAccess]; authorization is decided
- * solely by the authorize() method, which here mirrors its `$allow` argument.
- */
-#[Name( 'authorizeOnly' )]
-#[Description( 'Authorization decided solely by authorize()' )]
-class AuthorizeOnlyQuery {
- public function execute( bool $allow ): string {
- unset( $allow );
- return 'allowed';
- }
-
- /**
- * Authorize the call. Mirrors `$allow` so tests can drive both branches.
- *
- * @param bool $allow Whether to allow the call.
- */
- public function authorize( bool $allow ): bool {
- return $allow;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeThrowsQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeThrowsQuery.php
deleted file mode 100644
index aa26cae358a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/AuthorizeThrowsQuery.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\InvalidTokenException;
-
-/**
- * Authorization is decided solely by `authorize()`, which always throws. The
- * `$kind` argument selects the exception class so tests can verify the
- * resolver's exception-translation path for each (an `ApiException` carries
- * its custom code through; any other `Throwable` is masked behind
- * `INTERNAL_ERROR` with a generic message).
- */
-#[Name( 'authorizeThrows' )]
-#[Description( 'authorize() throws to verify exception translation' )]
-class AuthorizeThrowsQuery {
- public function execute(
- #[Description( 'Which exception class authorize() should raise.' )]
- string $kind,
- ): string {
- // Never reached — authorize() always throws.
- unset( $kind );
- return 'unreachable';
- }
-
- /**
- * Always throws. The `$kind` argument selects the exception class.
- *
- * @param string $kind Exception variety to raise.
- *
- * @throws ApiException When `$kind === 'api_exception'`.
- * @throws \RuntimeException Otherwise.
- */
- public function authorize( string $kind ): bool {
- if ( 'api_exception' === $kind ) {
- throw new ApiException( 'Authorize failed.', 'AUTH_FAILURE', array( 'detail' => 'extra' ), 403 );
- }
- if ( 'invalid_token' === $kind ) {
- throw new InvalidTokenException();
- }
- throw new \RuntimeException( 'Internals leaked from authorize.' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/ComposedAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/ComposedAuthorizeQuery.php
deleted file mode 100644
index 20a4b4ee59a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/ComposedAuthorizeQuery.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Composes a #[RequiredCapability] with a custom authorize(): the resolver
- * passes the cap-check result as the `$_preauthorized` infrastructure
- * argument, so this method can either short-circuit on the attribute's
- * decision or fall back to its own logic (here: an extra cap fallback).
- */
-#[Name( 'composedAuthorize' )]
-#[Description( 'Composes #[RequiredCapability] with authorize() via $_preauthorized' )]
-#[RequiredCapability( 'manage_options' )]
-class ComposedAuthorizeQuery {
- public function execute(): string {
- return 'composed';
- }
-
- /**
- * Allow when the attribute already passed (preauthorized) OR when the
- * caller has the edit_posts fallback cap.
- *
- * @param bool $_preauthorized True when current_user_can('manage_options') passed at the resolver level.
- */
- public function authorize( bool $_preauthorized ): bool {
- return $_preauthorized || current_user_can( 'edit_posts' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/IgnoredAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/IgnoredAuthorizeQuery.php
deleted file mode 100644
index 6c356a5fd7a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/IgnoredAuthorizeQuery.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * authorize() carries #[Ignore]; the builder must skip it and rely on
- * #[RequiredCapability] alone. The authorize() body returns `false`, so if
- * the builder *did* call it, every request would be rejected — making any
- * regression unmistakable.
- */
-#[Name( 'ignoredAuthorize' )]
-#[Description( 'authorize() with #[Ignore] is skipped; the cap check applies' )]
-#[RequiredCapability( 'manage_options' )]
-class IgnoredAuthorizeQuery {
- public function execute(): string {
- return 'cap enforced';
- }
-
- #[Ignore]
- public function authorize(): bool {
- return false;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareInternalQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareInternalQuery.php
deleted file mode 100644
index 28a0e61b5ce..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareInternalQuery.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Internal;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\RequiresInternalFlag;
-
-/**
- * Carries an `#[Internal]` metadata entry and is gated by
- * {@see RequiresInternalFlag}, which reads `$_metadata['query']['internal']`.
- * The gate should grant: this is the happy path for the metadata slot.
- */
-#[Name( 'metadataAwareInternalQuery' )]
-#[Description( 'Exercises RequiresInternalFlag against a class that carries #[Internal].' )]
-#[Internal]
-#[RequiresInternalFlag]
-class MetadataAwareInternalQuery {
- public function execute(): string {
- return 'ok-internal';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareNoFlagQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareNoFlagQuery.php
deleted file mode 100644
index aaf2d6639a4..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/MetadataAwareNoFlagQuery.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\RequiresInternalFlag;
-
-/**
- * Gated by {@see RequiresInternalFlag} but carries no `#[Internal]`
- * metadata. The gate should deny: this is the negative path for the
- * metadata slot — `$_metadata['query']` is empty, so the attribute's
- * "is internal?" check returns false.
- */
-#[Name( 'metadataAwareNoFlagQuery' )]
-#[Description( 'Exercises RequiresInternalFlag without the matching #[Internal] entry.' )]
-#[RequiresInternalFlag]
-class MetadataAwareNoFlagQuery {
- public function execute(): string {
- return 'ok-no-internal';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/OverriddenAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/OverriddenAuthorizeQuery.php
deleted file mode 100644
index 8fed8b63ace..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/OverriddenAuthorizeQuery.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\BaseManageOptionsQuery;
-
-/**
- * Inherits #[RequiredCapability('manage_options')] from its parent and
- * declares its own authorize(). This is the documented override mechanism:
- * authorize() takes precedence, the inherited cap is silently superseded
- * (no $_preauthorized parameter, so no composition).
- */
-#[Name( 'overriddenAuthorize' )]
-#[Description( 'authorize() supersedes the cap inherited from the parent' )]
-class OverriddenAuthorizeQuery extends BaseManageOptionsQuery {
- public function execute(): string {
- return 'authorize wins';
- }
-
- /**
- * Allow only callers with the edit_posts capability — independent of
- * the manage_options cap inherited from {@see BaseManageOptionsQuery}.
- */
- public function authorize(): bool {
- return current_user_can( 'edit_posts' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PrincipalAwareQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PrincipalAwareQuery.php
deleted file mode 100644
index 9f67ab8d4bc..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PrincipalAwareQuery.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Infrastructure\Principal;
-
-/**
- * Exercises the `_principal` infrastructure parameter on both authorize() and
- * execute().
- *
- * Returns the principal's user_login when authenticated, or 'anonymous' when
- * not (the underlying WP_User has ID === 0). Authorize() returns true
- * unconditionally — the test isn't gating access, just verifying the principal
- * flows through the typed channel.
- */
-#[Name( 'principalAware' )]
-#[Description( 'Echoes the principal user_login (or "anonymous").' )]
-class PrincipalAwareQuery {
- public function execute( Principal $_principal ): string {
- return $_principal->is_authenticated() ? $_principal->user->user_login : 'anonymous';
- }
-
- /**
- * Authorize the call. Always allows; the test reads the value out via execute().
- *
- * @param Principal $_principal The resolved principal.
- */
- public function authorize( Principal $_principal ): bool {
- unset( $_principal );
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PublicWidgetAccess.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PublicWidgetAccess.php
deleted file mode 100644
index edf0cb24b41..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Authorization/PublicWidgetAccess.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-/**
- * Publicly-accessible Widget query used by the field-level authorization tests
- * to reach a `Widget` instance without first passing through the class-level
- * `#[RequiredCapability]` gate on `GetWidget`. Lets the field-level gates on
- * `Widget`'s properties be exercised in isolation.
- */
-#[Name( 'publicWidget' )]
-#[Description( 'Fetch a widget without query-level gating; field-level gates apply.' )]
-#[PublicAccess]
-class PublicWidgetAccess {
- public function execute(): ?Widget {
- return Store::get_widget( 1 );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/FailingQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/FailingQuery.php
deleted file mode 100644
index 0b5854e683f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/FailingQuery.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-
-/**
- * Always throws, used to exercise the resolver's exception → GraphQL error
- * translation path. The argument selects which exception variety to raise.
- */
-#[Name( 'failing' )]
-#[Description( 'Always throws an exception' )]
-#[PublicAccess]
-class FailingQuery {
- public function execute(
- #[Description( 'What kind of failure to raise' )]
- string $kind = 'invalid_argument',
- ): string {
- switch ( $kind ) {
- case 'api_exception':
- throw new ApiException( 'Custom failure.', 'CUSTOM_FAILURE', array( 'detail' => 'extra' ), 418 );
- case 'invalid_argument':
- throw new \InvalidArgumentException( 'Bad input from caller.' );
- case 'runtime':
- default:
- throw new \RuntimeException( 'Something blew up.' );
- }
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetGreeting.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetGreeting.php
deleted file mode 100644
index a434effef65..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetGreeting.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-
-/**
- * Returns a greeting — exercises:
- * - scalar (string) return type, which the generator wraps in a result object.
- * - #[PublicAccess].
- */
-#[Name( 'greeting' )]
-#[Description( 'Build a greeting' )]
-#[PublicAccess]
-class GetGreeting {
- public function execute(
- #[Description( 'Who to greet (defaults to "world")' )]
- ?string $name = null,
- ): string {
- return sprintf( 'Hello, %s!', $name ?? 'world' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetIdentifiable.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetIdentifiable.php
deleted file mode 100644
index 077b70012a6..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetIdentifiable.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Api\Attributes\ReturnType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces\Named;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-
-/**
- * Returns an interface type — exercises #[ReturnType] (since PHP cannot
- * type-hint a trait, the method returns `object`).
- *
- * The argument toggles the concrete type returned so tests can verify the
- * interface's `resolveType` callback selects the right ObjectType.
- */
-#[Name( 'namedThing' )]
-#[Description( 'Return either a Widget or a Gadget, both of which implement Named' )]
-#[PublicAccess]
-class GetIdentifiable {
- #[ReturnType( Named::class )]
- public function execute(
- #[Description( 'Which kind of object to return' )]
- string $kind,
- ): object {
- if ( 'gadget' === $kind ) {
- return Store::build_gadget( 99, 'Sample Gadget', 7 );
- }
- Store::seed();
- return Store::get_widget( 1 );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetWidget.php
deleted file mode 100644
index bc839340ea1..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/GetWidget.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-#[Name( 'widget' )]
-#[Description( 'Fetch a single widget by ID' )]
-#[RequiredCapability( 'manage_options' )]
-class GetWidget {
- public function execute(
- #[Description( 'The ID of the widget to fetch' )]
- int $id,
- ): ?Widget {
- return Store::get_widget( $id );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/HiddenFlaggedQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/HiddenFlaggedQuery.php
deleted file mode 100644
index 46a3c62ba85..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/HiddenFlaggedQuery.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\HiddenFromMetadataQuery;
-use Automattic\WooCommerce\Api\Attributes\Metadata;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\RuntimeMetaProbe;
-
-/**
- * Public query that is itself hidden from `_apiMetadata` and carries a
- * `runtime_flag` metadata entry. The `by_query` field on its returned type is
- * gated on `$_metadata['query']['runtime_flag']`, so it only grants if the
- * hidden query's metadata still reaches the runtime gate (the discovery
- * opt-out must not blank the published `_query_metadata`).
- */
-#[Name( 'hiddenFlagged' )]
-#[Description( 'Probe query for runtime metadata visibility under #[HiddenFromMetadataQuery].' )]
-#[PublicAccess]
-#[Metadata( 'runtime_flag', true )]
-#[HiddenFromMetadataQuery]
-class HiddenFlaggedQuery {
- public function execute(): RuntimeMetaProbe {
- $probe = new RuntimeMetaProbe();
- $probe->by_type = 'type-ok';
- $probe->by_field = 'field-ok';
- $probe->by_query = 'query-ok';
- return $probe;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/IgnoredQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/IgnoredQuery.php
deleted file mode 100644
index 8e23e349406..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/IgnoredQuery.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-
-/**
- * Carries #[Ignore] so the ApiBuilder skips it entirely. Tests assert that
- * the generated schema does NOT expose any field for this class.
- */
-#[Ignore]
-#[PublicAccess]
-class IgnoredQuery {
- public function execute(): string {
- return 'should never be reachable';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/BaseManageOptionsQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/BaseManageOptionsQuery.php
deleted file mode 100644
index bcc6c2bc2fd..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/BaseManageOptionsQuery.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Abstract parent class carrying #[RequiredCapability('manage_options')].
- *
- * Auto-ignored by the builder because it is abstract, but its attribute is
- * still discoverable via reflection on derived classes — which is the whole
- * point of testing inheritance.
- */
-#[RequiredCapability( 'manage_options' )]
-abstract class BaseManageOptionsQuery {
- /**
- * Implemented by each concrete derived query.
- */
- abstract public function execute(): string;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedCapQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedCapQuery.php
deleted file mode 100644
index f25e75d4ae0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedCapQuery.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-
-/**
- * Inherits #[RequiredCapability('manage_options')] from its abstract parent
- * with no direct attribute of its own.
- */
-#[Name( 'inheritedCap' )]
-#[Description( 'Inherits manage_options from its abstract parent' )]
-class InheritedCapQuery extends BaseManageOptionsQuery {
- public function execute(): string {
- return 'inherited cap';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedFromInterfaceQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedFromInterfaceQuery.php
deleted file mode 100644
index 8c08c0f740a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedFromInterfaceQuery.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization\RequiresManageOptions;
-
-/**
- * Inherits #[RequiredCapability('manage_options')] from a PHP interface.
- */
-#[Name( 'inheritedFromInterface' )]
-#[Description( 'Inherits manage_options from a PHP interface' )]
-class InheritedFromInterfaceQuery implements RequiresManageOptions {
- public function execute(): string {
- return 'inherited from interface';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedPublicQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedPublicQuery.php
deleted file mode 100644
index 0fa91c1c8e1..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/InheritedPublicQuery.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization\PublicAccessTrait;
-
-/**
- * Inherits #[PublicAccess] via a trait. No direct authorization attribute.
- */
-#[Name( 'inheritedPublic' )]
-#[Description( 'Inherits PublicAccess via a trait' )]
-class InheritedPublicQuery {
- use PublicAccessTrait;
-
- public function execute(): string {
- return 'inherited public';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/MergedCapsQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/MergedCapsQuery.php
deleted file mode 100644
index 768710bfcf8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/MergedCapsQuery.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Authorization\RequiresEditPostsTrait;
-
-/**
- * Inherits caps from two sources at once: manage_options from its parent
- * class, edit_posts from its trait. The builder should require both.
- */
-#[Name( 'mergedCaps' )]
-#[Description( 'Merges caps from a parent class and a trait' )]
-class MergedCapsQuery extends BaseManageOptionsQuery {
- use RequiresEditPostsTrait;
-
- public function execute(): string {
- return 'merged caps';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/OverriddenCapQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/OverriddenCapQuery.php
deleted file mode 100644
index 8b9473224a8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/Inheritance/OverriddenCapQuery.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-
-/**
- * Carries a direct #[RequiredCapability] that should *override* the cap
- * inherited from its parent — only `manage_categories` should be enforced.
- */
-#[Name( 'overriddenCap' )]
-#[Description( 'Overrides the inherited manage_options with manage_categories' )]
-#[RequiredCapability( 'manage_categories' )]
-class OverriddenCapQuery extends BaseManageOptionsQuery {
- public function execute(): string {
- return 'overridden cap';
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgets.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgets.php
deleted file mode 100644
index 57ca4c0cf82..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgets.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\Unroll;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\Edge;
-use Automattic\WooCommerce\Api\Pagination\PageInfo;
-use Automattic\WooCommerce\Api\Pagination\PaginationParams;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\WidgetFilterInput;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-/**
- * Lists widgets, exercising:
- * - class-level #[Unroll] (via PaginationParams) on a parameter.
- * - parameter-level #[Unroll] on the filters argument.
- * - multiple #[RequiredCapability] attributes.
- * - the infrastructure `_query_info` parameter.
- * - #[ConnectionOf] on the execute method.
- */
-#[Name( 'widgets' )]
-#[Description( 'List widgets with cursor-based pagination' )]
-#[RequiredCapability( 'manage_options' )]
-#[RequiredCapability( 'edit_posts' )]
-class ListWidgets {
- #[ConnectionOf( Widget::class )]
- public function execute(
- PaginationParams $pagination,
- #[Unroll]
- WidgetFilterInput $filters,
- #[Description( 'A second filter applied after the unrolled ones' )]
- ?Priority $min_priority = null,
- ?array $_query_info = null,
- ): Connection {
- unset( $_query_info );
-
- $widgets = array_values( Store::all_widgets() );
-
- if ( null !== $filters->color ) {
- $widgets = array_values(
- array_filter(
- $widgets,
- static fn( Widget $w ): bool => $w->color === $filters->color
- )
- );
- }
- if ( null !== $filters->search ) {
- $needle = $filters->search;
- $widgets = array_values(
- array_filter(
- $widgets,
- static fn( Widget $w ): bool => str_contains( strtolower( $w->label ), strtolower( $needle ) )
- )
- );
- }
- if ( null !== $min_priority ) {
- $widgets = array_values(
- array_filter(
- $widgets,
- static fn( Widget $w ): bool => $w->priority === $min_priority
- )
- );
- }
-
- $total = count( $widgets );
-
- $limit = $pagination->first ?? $pagination->last ?? PaginationParams::get_default_page_size();
- $page = array_slice( $widgets, 0, $limit );
-
- $edges = array();
- $nodes = array();
- foreach ( $page as $widget ) {
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $widget->id );
- $edge->node = $widget;
- $edges[] = $edge;
- $nodes[] = $widget;
- }
-
- $page_info = new PageInfo();
- $page_info->has_next_page = count( $page ) < $total;
- $page_info->has_previous_page = false;
- $page_info->start_cursor = $edges[0]->cursor ?? null;
- $page_info->end_cursor = $edges[ count( $edges ) - 1 ]->cursor ?? null;
-
- $connection = new Connection();
- $connection->edges = $edges;
- $connection->nodes = $nodes;
- $connection->page_info = $page_info;
- $connection->total_count = $total;
-
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgetsArray.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgetsArray.php
deleted file mode 100644
index 528309bf2d4..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Queries/ListWidgetsArray.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-
-/**
- * Returns a plain (non-paginated) list of widgets — exercises the
- * `#[ArrayOf]` element-type declaration on a query's `execute()` *return*
- * value, which the generator turns into `[Widget!]!`.
- *
- * Regression coverage for the return-type list path: distinct from
- * `#[ConnectionOf]` (which {@see ListWidgets} covers) and from `#[ArrayOf]`
- * on properties / parameters (which {@see Widget} and the mutations cover).
- * Before this was fixed, an `array` return with `#[ArrayOf]` fell through to
- * the scalar fallback and emitted `String!`.
- */
-#[Name( 'widgetList' )]
-#[Description( 'List widgets without pagination.' )]
-#[PublicAccess]
-class ListWidgetsArray {
- /**
- * @return Widget[]
- */
- #[ArrayOf( Widget::class )]
- public function execute(): array {
- return array();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Scalars/DummyDateTime.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Scalars/DummyDateTime.php
deleted file mode 100644
index 207b854b831..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Scalars/DummyDateTime.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Scalars;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-/**
- * Custom scalar for ISO-8601 date/time strings used by the dummy fixture API.
- */
-#[Description( 'An ISO 8601 encoded date/time string used by the dummy API' )]
-class DummyDateTime {
- public static function serialize( mixed $value ): string {
- if ( ! $value instanceof \DateTimeInterface ) {
- throw new \InvalidArgumentException( 'DummyDateTime::serialize() expects a DateTimeInterface instance.' );
- }
- return $value->format( \DateTimeInterface::ATOM );
- }
-
- public static function parse( string $value ): \DateTimeImmutable {
- // Reject anything that is not a strict ATOM-formatted string. PHP's
- // free-form date parser would otherwise accept inputs like
- // '2024-06-15 08:30:00' which the scalar's contract disallows.
- $date = \DateTimeImmutable::createFromFormat( \DateTimeInterface::ATOM, $value );
- if ( false === $date || $date->format( \DateTimeInterface::ATOM ) !== $value ) {
- throw new \InvalidArgumentException( 'Invalid ISO 8601 date/time.' );
- }
- return $date;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Store.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Store.php
deleted file mode 100644
index 34d25eb83de..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Store.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi;
-
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Api\Pagination\Edge;
-use Automattic\WooCommerce\Api\Pagination\PageInfo;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Gadget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\WidgetReview;
-
-/**
- * In-memory fixture data backing the dummy queries / mutations.
- *
- * Carries #[Ignore] so the ApiBuilder skips it during discovery — it lives
- * inside the scanned namespace as a convenience helper, not as a code-API
- * type.
- */
-#[Ignore]
-final class Store {
- /**
- * @var array<int, Widget>
- */
- private static array $widgets = array();
-
- private static int $next_id = 1;
-
- public static function reset(): void {
- self::$widgets = array();
- self::$next_id = 1;
- self::seed();
- }
-
- public static function seed(): void {
- if ( empty( self::$widgets ) ) {
- self::create_widget( 'Alpha', Color::Red, 'alpha' );
- self::create_widget( 'Beta', Color::Green, 'beta' );
- }
- }
-
- public static function create_widget( string $label, Color $color, string $slug = '' ): Widget {
- $widget = new Widget();
- $widget->id = self::$next_id++;
- $widget->label = $label;
- $widget->slug = '' === $slug ? strtolower( $label ) : $slug;
- $widget->caption = null;
- $widget->color = $color;
- $widget->priority = Priority::Normal;
- $widget->tag_ids = array( 1, 2, 3 );
- $widget->featured_reviews = self::build_reviews( $widget->id, 1 );
- $widget->reviews = self::build_review_connection( $widget->id, 2 );
- $widget->date_created = '2024-01-01T00:00:00+00:00';
- $widget->price = '9.99';
- $widget->legacy_price = '8.50';
- $widget->internal_notes = 'do not expose';
-
- self::$widgets[ $widget->id ] = $widget;
- return $widget;
- }
-
- public static function get_widget( int $id ): ?Widget {
- return self::$widgets[ $id ] ?? null;
- }
-
- public static function delete_widget( int $id ): bool {
- if ( ! isset( self::$widgets[ $id ] ) ) {
- return false;
- }
- unset( self::$widgets[ $id ] );
- return true;
- }
-
- /**
- * @return array<int, Widget>
- */
- public static function all_widgets(): array {
- return self::$widgets;
- }
-
- public static function build_gadget( int $id, string $label, int $parts ): Gadget {
- $gadget = new Gadget();
- $gadget->id = $id;
- $gadget->label = $label;
- $gadget->parts_count = $parts;
- return $gadget;
- }
-
- /**
- * @return WidgetReview[]
- */
- private static function build_reviews( int $widget_id, int $count ): array {
- $reviews = array();
- for ( $i = 1; $i <= $count; $i++ ) {
- $review = new WidgetReview();
- $review->id = $widget_id * 100 + $i;
- $review->body = sprintf( 'Featured review %d for widget %d', $i, $widget_id );
- $review->score = 5;
- $reviews[] = $review;
- }
- return $reviews;
- }
-
- private static function build_review_connection( int $widget_id, int $count ): Connection {
- $edges = array();
- $nodes = array();
- for ( $i = 1; $i <= $count; $i++ ) {
- $review = new WidgetReview();
- $review->id = $widget_id * 1000 + $i;
- $review->body = sprintf( 'Review %d for widget %d', $i, $widget_id );
- $review->score = 4;
-
- $edge = new Edge();
- $edge->cursor = base64_encode( (string) $review->id );
- $edge->node = $review;
-
- $edges[] = $edge;
- $nodes[] = $review;
- }
-
- $page_info = new PageInfo();
- $page_info->has_next_page = false;
- $page_info->has_previous_page = false;
- $page_info->start_cursor = $edges[0]->cursor ?? null;
- $page_info->end_cursor = $edges[ count( $edges ) - 1 ]->cursor ?? null;
-
- $connection = new Connection();
- $connection->edges = $edges;
- $connection->nodes = $nodes;
- $connection->page_info = $page_info;
- $connection->total_count = $count;
- return $connection;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Gadget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Gadget.php
deleted file mode 100644
index da73bf2a952..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Gadget.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\Name;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces\Named;
-
-/**
- * A second concrete implementation of {@see Named}, used to verify that
- * interface dispatch (`resolveType`) works across multiple implementors.
- *
- * Carries a class-level #[Name] override so the GraphQL type is `GadgetType`.
- */
-#[Name( 'GadgetType' )]
-#[Description( 'A dummy gadget that uses a class-level #[Name] override' )]
-class Gadget {
- use Named;
-
- #[Description( 'How many parts the gadget contains' )]
- public int $parts_count;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/OperationResult.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/OperationResult.php
deleted file mode 100644
index 5170bdeeb9f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/OperationResult.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-
-#[Description( 'The result of a generic operation' )]
-class OperationResult {
- #[Description( 'Whether the operation succeeded' )]
- public bool $success;
-
- #[Description( 'A human-readable status message' )]
- public string $message;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/RuntimeMetaProbe.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/RuntimeMetaProbe.php
deleted file mode 100644
index 793fb68200c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/RuntimeMetaProbe.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types;
-
-use Automattic\WooCommerce\Api\Attributes\HiddenFromMetadataQuery;
-use Automattic\WooCommerce\Api\Attributes\Metadata;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\GrantsIfMetadataFlag;
-
-/**
- * Output type for the runtime-metadata-visibility regression. The type, one of
- * its fields, and the query that returns it ({@see \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\HiddenFlaggedQuery})
- * all opt out of `_apiMetadata` via `#[HiddenFromMetadataQuery]` while carrying
- * a `runtime_flag` metadata entry. Each gated field reads a different
- * `$_metadata` slice and must still grant, proving the discovery opt-out does
- * not blank the runtime metadata.
- */
-#[Metadata( 'runtime_flag', true )]
-#[HiddenFromMetadataQuery]
-class RuntimeMetaProbe {
- /**
- * Reads the `type` slice: the enclosing (hidden) type carries `runtime_flag`.
- */
- #[GrantsIfMetadataFlag( 'type' )]
- public ?string $by_type;
-
- /**
- * Reads the `field` slice: this field is itself hidden and carries `runtime_flag`.
- */
- #[Metadata( 'runtime_flag', true )]
- #[HiddenFromMetadataQuery]
- #[GrantsIfMetadataFlag( 'field' )]
- public ?string $by_field;
-
- /**
- * Reads the `query` slice: the (hidden) query that resolves this type carries `runtime_flag`.
- */
- #[GrantsIfMetadataFlag( 'query' )]
- public ?string $by_query;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Widget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Widget.php
deleted file mode 100644
index 33762826444..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/Widget.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types;
-
-use Automattic\WooCommerce\Api\Attributes\ArrayOf;
-use Automattic\WooCommerce\Api\Attributes\ConnectionOf;
-use Automattic\WooCommerce\Api\Attributes\Deprecated;
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Api\Attributes\HiddenFromMetadataQuery;
-use Automattic\WooCommerce\Api\Attributes\Ignore;
-use Automattic\WooCommerce\Api\Attributes\Parameter;
-use Automattic\WooCommerce\Api\Attributes\ParameterDescription;
-use Automattic\WooCommerce\Api\Attributes\PublicAccess;
-use Automattic\WooCommerce\Api\Attributes\RequiredCapability;
-use Automattic\WooCommerce\Api\Attributes\ScalarType;
-use Automattic\WooCommerce\Api\Pagination\Connection;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces\Named;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Metadata\VisibleSampleMetadata;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Scalars\DummyDateTime;
-
-/**
- * A widget — exercises every attribute applicable to an output type.
- */
-#[Description( 'A dummy widget that exercises every output-type attribute' )]
-class Widget {
- use Named;
-
- #[Description( 'A short slug' )]
- public string $slug;
-
- #[Description( 'An optional caption' )]
- #[VisibleSampleMetadata]
- #[RequiredCapability( 'manage_woocommerce' )]
- public ?string $caption;
-
- #[Description( 'The widget color' )]
- public Color $color;
-
- #[Description( 'Priority assigned to this widget' )]
- public Priority $priority;
-
- #[Description( 'Tag IDs assigned to this widget' )]
- #[ArrayOf( 'int' )]
- #[PublicAccess]
- public array $tag_ids;
-
- #[Description( 'Notable comments left on this widget' )]
- #[ArrayOf( WidgetReview::class )]
- public array $featured_reviews;
-
- #[Description( 'Reviews of the widget' )]
- #[ConnectionOf( WidgetReview::class )]
- public Connection $reviews;
-
- #[Description( 'When the widget was created' )]
- #[ScalarType( DummyDateTime::class )]
- public ?string $date_created;
-
- /**
- * Demonstrates a forwarded #[Parameter] argument on a property.
- *
- * The matching #[ParameterDescription] is split out below to exercise
- * that attribute's "augment without redeclaring the type" path.
- */
- #[Description( 'The widget price' )]
- #[Parameter( name: 'formatted', type: 'bool', default: false )]
- #[ParameterDescription( name: 'formatted', description: 'When true, prepend a $ sign' )]
- public string $price;
-
- #[Description( 'A field flagged for removal' )]
- #[Deprecated( 'Use price instead.' )]
- #[HiddenFromMetadataQuery]
- public string $legacy_price;
-
- #[Ignore]
- public ?string $internal_notes;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/WidgetReview.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/WidgetReview.php
deleted file mode 100644
index b077d319819..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApi/Types/WidgetReview.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types;
-
-use Automattic\WooCommerce\Api\Attributes\Description;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Interfaces\Identifiable;
-
-/**
- * A review of a widget.
- */
-#[Description( 'A review left for a widget' )]
-class WidgetReview {
- use Identifiable;
-
- #[Description( 'The body of the review' )]
- public string $body;
-
- #[Description( 'A score between 0 and 5' )]
- public int $score;
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLController.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLController.php
deleted file mode 100644
index 6ee6c8305cf..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLController.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Schema;
-
-class GraphQLController extends \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase {
- protected function build_schema(): Schema {
- return new Schema(
- array(
- 'query' => RootQueryType::get(),
- 'mutation' => RootMutationType::get(),
- 'types' => TypeRegistry::get_interface_implementors(),
- )
- );
- }
-
- protected function get_class_resolver_fqcn(): ?string {
- return \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::class;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/CreateWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/CreateWidget.php
deleted file mode 100644
index 45487720e96..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/CreateWidget.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations\CreateWidget as CreateWidgetCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input\CreateWidget as CreateWidgetInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateWidget {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( WidgetType::get() ),
- 'description' => __( 'Create a new widget', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( CreateWidgetInput::get() ),
- 'description' => __( 'The data for the new widget', 'woocommerce' ),
- ),
- 'related_inputs' => array(
- 'type' => Type::listOf( Type::nonNull( CreateWidgetInput::get() ) ),
- 'description' => __( 'Related widget inputs for array input generation coverage', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( CreateWidgetCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_create_widget_input( $args['input'] );
- }
- if ( array_key_exists( 'related_inputs', $args ) ) {
- $execute_args['related_inputs'] = $args['related_inputs'];
- }
-
- if ( isset( $execute_args['input'] ) && $execute_args['input'] instanceof \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput ) {
- $_parent = $execute_args['input'];
- if ( $_parent->was_provided( 'weight' ) ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(),
- 'field' => array(),
- );
- $_args = $args;
- if ( ! ( ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'CreateWidgetInput', 'weight', 'RequiredCapability' );
- }
- }
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-
- private static function convert_create_widget_input( array $data ): \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput {
- $input = new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput();
-
- if ( array_key_exists( 'label', $data ) ) {
- $input->mark_provided( 'label' );
- $input->label = $data['label'];
- }
- if ( array_key_exists( 'weight', $data ) ) {
- $input->mark_provided( 'weight' );
- $input->weight = $data['weight'];
- }
- if ( array_key_exists( 'color', $data ) ) {
- $input->mark_provided( 'color' );
- $input->color = $data['color'];
- }
- if ( array_key_exists( 'tag_ids', $data ) ) {
- $input->mark_provided( 'tag_ids' );
- $input->tag_ids = $data['tag_ids'];
- }
- if ( array_key_exists( 'expires_at', $data ) ) {
- $input->mark_provided( 'expires_at' );
- $input->expires_at = $data['expires_at'];
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/DeleteWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/DeleteWidget.php
deleted file mode 100644
index b8464304a1f..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/DeleteWidget.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations\DeleteWidget as DeleteWidgetCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\OperationResult as OperationResultType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class DeleteWidget {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( OperationResultType::get() ),
- 'description' => __( 'Delete a widget', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The widget id to delete', 'woocommerce' ),
- ),
- 'force' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'When true, ignore \"not found\" errors', 'woocommerce' ),
- 'defaultValue' => false,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( DeleteWidgetCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
- if ( array_key_exists( 'force', $args ) ) {
- $execute_args['force'] = $args['force'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/Increment.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/Increment.php
deleted file mode 100644
index 8afdff89e90..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/Increment.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations\Increment as IncrementCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Increment {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'IncrementResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::int() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Increment a value by an optional amount', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(
- 'value' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The starting value', 'woocommerce' ),
- ),
- 'by' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'How much to add', 'woocommerce' ),
- 'defaultValue' => 1,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( IncrementCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'value', $args ) ) {
- $execute_args['value'] = $args['value'];
- }
- if ( array_key_exists( 'by', $args ) ) {
- $execute_args['by'] = $args['by'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/PublicCreateWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/PublicCreateWidget.php
deleted file mode 100644
index baa07014195..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLMutations/PublicCreateWidget.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Mutations\PublicCreateWidget as PublicCreateWidgetCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input\CreateWidget as CreateWidgetInput;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PublicCreateWidget {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( WidgetType::get() ),
- 'description' => __( 'Create a widget without query-level gating; input-side gates apply.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(
- 'input' => array(
- 'type' => Type::nonNull( CreateWidgetInput::get() ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( PublicCreateWidgetCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'input', $args ) ) {
- $execute_args['input'] = self::convert_create_widget_input( $args['input'] );
- }
-
- if ( isset( $execute_args['input'] ) && $execute_args['input'] instanceof \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput ) {
- $_parent = $execute_args['input'];
- if ( $_parent->was_provided( 'weight' ) ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(),
- 'field' => array(),
- );
- $_args = $args;
- if ( ! ( ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'CreateWidgetInput', 'weight', 'RequiredCapability' );
- }
- }
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-
- private static function convert_create_widget_input( array $data ): \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput {
- $input = new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\CreateWidgetInput();
-
- if ( array_key_exists( 'label', $data ) ) {
- $input->mark_provided( 'label' );
- $input->label = $data['label'];
- }
- if ( array_key_exists( 'weight', $data ) ) {
- $input->mark_provided( 'weight' );
- $input->weight = $data['weight'];
- }
- if ( array_key_exists( 'color', $data ) ) {
- $input->mark_provided( 'color' );
- $input->color = $data['color'];
- }
- if ( array_key_exists( 'tag_ids', $data ) ) {
- $input->mark_provided( 'tag_ids' );
- $input->tag_ids = $data['tag_ids'];
- }
- if ( array_key_exists( 'expires_at', $data ) ) {
- $input->mark_provided( 'expires_at' );
- $input->expires_at = $data['expires_at'];
- }
-
- return $input;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeOnlyQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeOnlyQuery.php
deleted file mode 100644
index 0d77556b7c0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeOnlyQuery.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\AuthorizeOnlyQuery as AuthorizeOnlyQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class AuthorizeOnlyQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'AuthorizeOnlyQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Authorization decided solely by authorize()', 'woocommerce' ),
- 'args' => array(
- 'allow' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( AuthorizeOnlyQueryCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'allow', $args ) ) {
- $execute_args['allow'] = $args['allow'];
- }
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array(
- 'allow' => $execute_args['allow'],
- )
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeThrowsQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeThrowsQuery.php
deleted file mode 100644
index 307633fcd51..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/AuthorizeThrowsQuery.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\AuthorizeThrowsQuery as AuthorizeThrowsQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class AuthorizeThrowsQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'AuthorizeThrowsQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'authorize() throws to verify exception translation', 'woocommerce' ),
- 'args' => array(
- 'kind' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'Which exception class authorize() should raise.', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( AuthorizeThrowsQueryCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'kind', $args ) ) {
- $execute_args['kind'] = $args['kind'];
- }
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array(
- 'kind' => $execute_args['kind'],
- )
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ComposedAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ComposedAuthorizeQuery.php
deleted file mode 100644
index d3b6b81c34b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ComposedAuthorizeQuery.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\ComposedAuthorizeQuery as ComposedAuthorizeQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ComposedAuthorizeQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'ComposedAuthorizeQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Composes #[RequiredCapability] with authorize() via $_preauthorized', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( ComposedAuthorizeQueryCommand::class );
-
- $execute_args = array();
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array(
- '_preauthorized' => self::compute_preauthorized( $context['principal'] ),
- )
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/FailingQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/FailingQuery.php
deleted file mode 100644
index 726e944a899..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/FailingQuery.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\FailingQuery as FailingQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class FailingQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'FailingQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Always throws an exception', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(
- 'kind' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'What kind of failure to raise', 'woocommerce' ),
- 'defaultValue' => 'invalid_argument',
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( FailingQueryCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'kind', $args ) ) {
- $execute_args['kind'] = $args['kind'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetGreeting.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetGreeting.php
deleted file mode 100644
index f47a54ff2ce..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetGreeting.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\GetGreeting as GetGreetingCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class GetGreeting {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'GetGreetingResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Build a greeting', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(
- 'name' => array(
- 'type' => Type::string(),
- 'description' => __( 'Who to greet (defaults to \"world\")', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( GetGreetingCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'name', $args ) ) {
- $execute_args['name'] = $args['name'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetIdentifiable.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetIdentifiable.php
deleted file mode 100644
index c45f8bb0b68..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetIdentifiable.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\GetIdentifiable as GetIdentifiableCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class GetIdentifiable {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( NamedInterface::get() ),
- 'description' => __( 'Return either a Widget or a Gadget, both of which implement Named', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(
- 'kind' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'Which kind of object to return', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( GetIdentifiableCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'kind', $args ) ) {
- $execute_args['kind'] = $args['kind'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetWidget.php
deleted file mode 100644
index 77bf79f5b05..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/GetWidget.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\GetWidget as GetWidgetCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class GetWidget {
- public static function get_field_definition(): array {
- return array(
- 'type' => WidgetType::get(),
- 'description' => __( 'Fetch a single widget by ID', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- ),
- 'args' => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The ID of the widget to fetch', 'woocommerce' ),
- ),
- ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( GetWidgetCommand::class );
-
- $execute_args = array();
- if ( array_key_exists( 'id', $args ) ) {
- $execute_args['id'] = $args['id'];
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/HiddenFlaggedQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/HiddenFlaggedQuery.php
deleted file mode 100644
index 7558220b79c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/HiddenFlaggedQuery.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\HiddenFlaggedQuery as HiddenFlaggedQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\RuntimeMetaProbe as RuntimeMetaProbeType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class HiddenFlaggedQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( RuntimeMetaProbeType::get() ),
- 'description' => __( 'Probe query for runtime metadata visibility under #[HiddenFromMetadataQuery].', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array(
- 'runtime_flag' => true,
- );
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( HiddenFlaggedQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/IgnoredAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/IgnoredAuthorizeQuery.php
deleted file mode 100644
index 49759a0e7b6..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/IgnoredAuthorizeQuery.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\IgnoredAuthorizeQuery as IgnoredAuthorizeQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class IgnoredAuthorizeQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'IgnoredAuthorizeQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'authorize() with #[Ignore] is skipped; the cap check applies', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( IgnoredAuthorizeQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedCapQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedCapQuery.php
deleted file mode 100644
index d20ceed9050..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedCapQuery.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\InheritedCapQuery as InheritedCapQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class InheritedCapQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'InheritedCapQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Inherits manage_options from its abstract parent', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( InheritedCapQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedFromInterfaceQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedFromInterfaceQuery.php
deleted file mode 100644
index 773de3237c8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedFromInterfaceQuery.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\InheritedFromInterfaceQuery as InheritedFromInterfaceQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class InheritedFromInterfaceQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'InheritedFromInterfaceQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Inherits manage_options from a PHP interface', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( InheritedFromInterfaceQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedPublicQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedPublicQuery.php
deleted file mode 100644
index db5fac27d19..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/InheritedPublicQuery.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\InheritedPublicQuery as InheritedPublicQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class InheritedPublicQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'InheritedPublicQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Inherits PublicAccess via a trait', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( InheritedPublicQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgets.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgets.php
deleted file mode 100644
index 081ff2c36fa..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgets.php
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\ListWidgets as ListWidgetsCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination\WidgetConnection as WidgetConnectionType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Priority as PriorityType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ListWidgets {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( WidgetConnectionType::get() ),
- 'description' => __( 'List widgets with cursor-based pagination', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_options',
- ),
- ),
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'edit_posts',
- ),
- ),
- ),
- 'args' => array(
- 'first' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the first N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'last' => array(
- 'type' => Type::int(),
- 'description' => __( 'Return the last N results. Must be between 0 and 100.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'after' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results after this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'before' => array(
- 'type' => Type::string(),
- 'description' => __( 'Return results before this cursor.', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'search' => array(
- 'type' => Type::string(),
- 'description' => __( 'A free-text search term', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'color' => array(
- 'type' => ColorType::get(),
- 'description' => __( 'Filter widgets by color', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- 'min_priority' => array(
- 'type' => PriorityType::get(),
- 'description' => __( 'A second filter applied after the unrolled ones', 'woocommerce' ),
- 'defaultValue' => null,
- ),
- ),
- 'complexity' => ResolverHelpers::complexity_from_pagination( ... ),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( ListWidgetsCommand::class );
-
- $query_info = QueryInfoExtractor::extract_from_info( $info, $args );
- $execute_args = array();
- $execute_args['pagination'] = ResolverHelpers::create_pagination_params( $args );
- $execute_args['filters'] = ResolverHelpers::create_input(
- fn() => new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\InputTypes\WidgetFilterInput(
- search: $args['search'] ?? null,
- color: $args['color'],
- )
- );
- if ( array_key_exists( 'min_priority', $args ) ) {
- $execute_args['min_priority'] = $args['min_priority'];
- }
- $execute_args['_query_info'] = $query_info;
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal ) && ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'edit_posts' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgetsArray.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgetsArray.php
deleted file mode 100644
index 0458a0d91c0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/ListWidgetsArray.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\ListWidgetsArray as ListWidgetsArrayCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class ListWidgetsArray {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Widget::get() ) ) ),
- 'description' => __( 'List widgets without pagination.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( ListWidgetsArrayCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MergedCapsQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MergedCapsQuery.php
deleted file mode 100644
index 49913ff5fd8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MergedCapsQuery.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\MergedCapsQuery as MergedCapsQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class MergedCapsQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'MergedCapsQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Merges caps from a parent class and a trait', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( MergedCapsQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal ) && ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'edit_posts' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareInternalQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareInternalQuery.php
deleted file mode 100644
index 52ed18eb0b6..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareInternalQuery.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\MetadataAwareInternalQuery as MetadataAwareInternalQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class MetadataAwareInternalQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'MetadataAwareInternalQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( '[Internal] Exercises RequiresInternalFlag against a class that carries #[Internal].', 'woocommerce' ),
- 'metadata' => array(
- 'internal' => true,
- ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiresInternalFlag',
- 'args' => array(),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array(
- 'internal' => true,
- );
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( MetadataAwareInternalQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\RequiresInternalFlag() )->authorize(
- $principal,
- _metadata: array(
- 'query' =>
- array(
- 'internal' => true,
- ),
- )
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareNoFlagQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareNoFlagQuery.php
deleted file mode 100644
index 176a15b6e9b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/MetadataAwareNoFlagQuery.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\MetadataAwareNoFlagQuery as MetadataAwareNoFlagQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class MetadataAwareNoFlagQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'MetadataAwareNoFlagQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Exercises RequiresInternalFlag without the matching #[Internal] entry.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiresInternalFlag',
- 'args' => array(),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( MetadataAwareNoFlagQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\RequiresInternalFlag() )->authorize(
- $principal,
- _metadata: array(
- 'query' =>
- array(),
- )
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenAuthorizeQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenAuthorizeQuery.php
deleted file mode 100644
index 18c1e5c0d1d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenAuthorizeQuery.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\OverriddenAuthorizeQuery as OverriddenAuthorizeQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class OverriddenAuthorizeQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'OverriddenAuthorizeQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'authorize() supersedes the cap inherited from the parent', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( OverriddenAuthorizeQueryCommand::class );
-
- $execute_args = array();
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array()
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_options' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenCapQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenCapQuery.php
deleted file mode 100644
index 95240d9935c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/OverriddenCapQuery.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Inheritance\OverriddenCapQuery as OverriddenCapQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class OverriddenCapQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'OverriddenCapQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Overrides the inherited manage_options with manage_categories', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_categories',
- ),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Standalone authorization gate: no authorize() method on the command,
- // so the autodiscovered authorization attributes are the sole guard.
- if ( ! self::compute_preauthorized( $context['principal'] ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( OverriddenCapQueryCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_categories' ) )->authorize( $principal );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PrincipalAwareQuery.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PrincipalAwareQuery.php
deleted file mode 100644
index 4d1c6cd6d8e..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PrincipalAwareQuery.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\PrincipalAwareQuery as PrincipalAwareQueryCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PrincipalAwareQuery {
- public static function get_field_definition(): array {
- return array(
- 'type' => Type::nonNull(
- new \Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType(
- array(
- 'name' => 'PrincipalAwareQueryResult',
- 'fields' => array(
- 'result' => array( 'type' => Type::nonNull( Type::string() ) ),
- ),
- )
- )
- ),
- 'description' => __( 'Echoes the principal user_login (or \"anonymous\").', 'woocommerce' ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( PrincipalAwareQueryCommand::class );
-
- $execute_args = array();
- $execute_args['_principal'] = $context['principal'];
-
- if ( ! ResolverHelpers::authorize_command(
- $command,
- array(
- '_principal' => $context['principal'],
- )
- ) ) {
- throw ResolverHelpers::build_authorization_error( $context['principal'] );
- }
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return array( 'result' => $result );
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PublicWidgetAccess.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PublicWidgetAccess.php
deleted file mode 100644
index 9c3c66a9a66..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLQueries/PublicWidgetAccess.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Queries\Authorization\PublicWidgetAccess as PublicWidgetAccessCommand;
-use Automattic\WooCommerce\Api\Infrastructure\QueryInfoExtractor;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ResolveInfo;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PublicWidgetAccess {
- public static function get_field_definition(): array {
- return array(
- 'type' => WidgetType::get(),
- 'description' => __( 'Fetch a widget without query-level gating; field-level gates apply.', 'woocommerce' ),
- 'authorization' => array(
- array(
- 'attribute' => 'PublicAccess',
- 'args' => array(),
- ),
- ),
- 'args' => array(),
- 'resolve' => array( self::class, 'resolve' ),
- );
- }
-
- public static function resolve( mixed $root, array $args, mixed $context, ResolveInfo $info ): mixed {
- // Publish the root query's metadata so downstream field-level
- // authorization gates can read it via `$_metadata['query']`.
- // $context is an ArrayObject (see GraphQLController::process_request())
- // so the mutation propagates to nested resolvers.
- $context['_query_metadata'] = array();
-
- $command = \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver::resolve_class( PublicWidgetAccessCommand::class );
-
- $execute_args = array();
-
- $result = ResolverHelpers::execute_command( $command, $execute_args );
-
- return $result;
- }
-
- /**
- * Compute the value `_preauthorized` would carry for a given principal —
- * the AND of the autodiscovered authorization attributes' authorize()
- * outcomes on this command. Single source of truth for both the resolver's
- * own gates and external (code-API) callers asking about authorization
- * without going through GraphQL execution.
- *
- * Returns true vacuously when the command has no authorization attributes
- * (in that case authorize() on the command is the sole guard, and that
- * method should be consulted instead).
- */
- public static function compute_preauthorized( \Automattic\WooCommerce\Api\Infrastructure\Principal $principal ): bool {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Color.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Color.php
deleted file mode 100644
index 34c14dc34fe..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Color.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Color as ColorEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class Color {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'Color',
- 'description' => __( 'A simple color palette', 'woocommerce' ),
- 'values' => array(
- 'RED' => array(
- 'value' => ColorEnum::Red,
- 'description' => __( 'Red', 'woocommerce' ),
- ),
- 'GREEN' => array(
- 'value' => ColorEnum::Green,
- 'description' => __( 'Green', 'woocommerce' ),
- ),
- 'BLUE' => array(
- 'value' => ColorEnum::Blue,
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Priority.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Priority.php
deleted file mode 100644
index 9214a2d596d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Enums/Priority.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Enums\Priority as PriorityEnum;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\EnumType;
-
-class Priority {
- private static ?EnumType $instance = null;
-
- public static function get(): EnumType {
- if ( null === self::$instance ) {
- self::$instance = new EnumType(
- array(
- 'name' => 'TaskPriority',
- 'description' => __( 'Priority level for a task', 'woocommerce' ),
- 'values' => array(
- 'LOW' => array(
- 'value' => PriorityEnum::Low,
- 'description' => __( 'Low priority', 'woocommerce' ),
- ),
- 'NORMAL_PRIORITY' => array(
- 'value' => PriorityEnum::Normal,
- ),
- 'HIGH' => array(
- 'value' => PriorityEnum::High,
- 'description' => __( 'High priority', 'woocommerce' ),
- 'deprecationReason' => 'Use NORMAL_PRIORITY instead.',
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/CreateWidget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/CreateWidget.php
deleted file mode 100644
index 0e7168cd0c0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/CreateWidget.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Scalars\DummyDateTime as DummyDateTimeType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class CreateWidget {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'CreateWidgetInput',
- 'description' => __( 'Data needed to create a new widget', 'woocommerce' ),
- 'fields' => fn() => array(
- 'label' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The widget label', 'woocommerce' ),
- ),
- 'weight' => array(
- 'type' => Type::int(),
- 'description' => __( 'Optional weight in grams', 'woocommerce' ),
- ),
- 'color' => array(
- 'type' => Type::nonNull( ColorType::get() ),
- 'description' => __( 'The widget color', 'woocommerce' ),
- ),
- 'tag_ids' => array(
- 'type' => Type::listOf( Type::nonNull( Type::int() ) ),
- 'description' => __( 'Tag IDs to attach to the widget', 'woocommerce' ),
- ),
- 'expires_at' => array(
- 'type' => DummyDateTimeType::get(),
- 'description' => __( 'When the widget should expire', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/WidgetFilter.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/WidgetFilter.php
deleted file mode 100644
index d9c7cd7f71c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Input/WidgetFilter.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Input;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InputObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetFilter {
- private static ?InputObjectType $instance = null;
-
- public static function get(): InputObjectType {
- if ( null === self::$instance ) {
- self::$instance = new InputObjectType(
- array(
- 'name' => 'WidgetFilterArgs',
- 'description' => __( 'Filters applied to a widget listing', 'woocommerce' ),
- 'fields' => fn() => array(
- 'search' => array(
- 'type' => Type::string(),
- 'description' => __( 'A free-text search term', 'woocommerce' ),
- ),
- 'color' => array(
- 'type' => ColorType::get(),
- 'description' => __( 'Filter widgets by color', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Identifiable.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Identifiable.php
deleted file mode 100644
index a974f84529b..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Identifiable.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\WidgetReview as WidgetReviewType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Identifiable {
- private static ?InterfaceType $instance = null;
-
- public static function get(): InterfaceType {
- if ( null === self::$instance ) {
- self::$instance = new InterfaceType(
- array(
- 'name' => 'HasId',
- 'description' => __( 'An object with a numeric identifier', 'woocommerce' ),
- 'fields' => fn() => array(
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier', 'woocommerce' ),
- ),
- ),
- 'resolveType' => function ( $value ) {
- $class = get_class( $value );
- $map = array(
- 'Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\WidgetReview' => WidgetReviewType::get(),
- );
- return $map[ $class ] ?? null;
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Named.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Named.php
deleted file mode 100644
index 6aa59d131e2..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Interfaces/Named.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Gadget as GadgetType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\InterfaceType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Named {
- private static ?InterfaceType $instance = null;
-
- public static function get(): InterfaceType {
- if ( null === self::$instance ) {
- self::$instance = new InterfaceType(
- array(
- 'name' => 'Named',
- 'description' => __( 'An object with a human-readable label', 'woocommerce' ),
- 'fields' => fn() => array(
- 'label' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The display label for this object', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier', 'woocommerce' ),
- ),
- ),
- 'resolveType' => function ( $value ) {
- $class = get_class( $value );
- $map = array(
- 'Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Gadget' => GadgetType::get(),
- 'Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Types\Widget' => WidgetType::get(),
- );
- return $map[ $class ] ?? null;
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Gadget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Gadget.php
deleted file mode 100644
index 0ae78148812..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Gadget.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Gadget {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'GadgetType',
- 'description' => __( 'A dummy gadget that uses a class-level #[Name] override', 'woocommerce' ),
- 'interfaces' => fn() => array(
- NamedInterface::get(),
- ),
- 'fields' => fn() => array(
- 'parts_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'How many parts the gadget contains', 'woocommerce' ),
- ),
- 'label' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The display label for this object', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/OperationResult.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/OperationResult.php
deleted file mode 100644
index 0ab0cd9b501..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/OperationResult.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class OperationResult {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'OperationResult',
- 'description' => __( 'The result of a generic operation', 'woocommerce' ),
- 'fields' => fn() => array(
- 'success' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- 'description' => __( 'Whether the operation succeeded', 'woocommerce' ),
- ),
- 'message' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'A human-readable status message', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/RuntimeMetaProbe.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/RuntimeMetaProbe.php
deleted file mode 100644
index d75e5c44fca..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/RuntimeMetaProbe.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class RuntimeMetaProbe {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'RuntimeMetaProbe',
- 'fields' => fn() => array(
- 'by_type' => array(
- 'type' => Type::string(),
- 'authorization' => array(
- array(
- 'attribute' => 'GrantsIfMetadataFlag',
- 'args' => array(
- 0 => 'type',
- ),
- ),
- ),
- 'resolve' => function ( $parent, $args, $context ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(
- 'runtime_flag' => true,
- ),
- 'field' => array(),
- );
- $_args = $args;
- $_parent = $parent;
- if ( ! ( ( new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\GrantsIfMetadataFlag( 'type' ) )->authorize( $principal, _metadata: $_metadata ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'RuntimeMetaProbe', 'by_type', 'GrantsIfMetadataFlag' );
- }
- return $parent->by_type;
- },
- ),
- 'by_field' => array(
- 'type' => Type::string(),
- 'resolve' => function ( $parent, $args, $context ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(
- 'runtime_flag' => true,
- ),
- 'field' => array(
- 'runtime_flag' => true,
- ),
- );
- $_args = $args;
- $_parent = $parent;
- if ( ! ( ( new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\GrantsIfMetadataFlag( 'field' ) )->authorize( $principal, _metadata: $_metadata ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'RuntimeMetaProbe', 'by_field', 'GrantsIfMetadataFlag' );
- }
- return $parent->by_field;
- },
- ),
- 'by_query' => array(
- 'type' => Type::string(),
- 'authorization' => array(
- array(
- 'attribute' => 'GrantsIfMetadataFlag',
- 'args' => array(
- 0 => 'query',
- ),
- ),
- ),
- 'resolve' => function ( $parent, $args, $context ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(
- 'runtime_flag' => true,
- ),
- 'field' => array(),
- );
- $_args = $args;
- $_parent = $parent;
- if ( ! ( ( new \Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Attributes\GrantsIfMetadataFlag( 'query' ) )->authorize( $principal, _metadata: $_metadata ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'RuntimeMetaProbe', 'by_query', 'GrantsIfMetadataFlag' );
- }
- return $parent->by_query;
- },
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Widget.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Widget.php
deleted file mode 100644
index f4fa31ea6a3..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/Widget.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Color as ColorType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Enums\Priority as PriorityType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\WidgetReview;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination\WidgetReviewConnection as WidgetReviewConnectionType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Scalars\DummyDateTime as DummyDateTimeType;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Named as NamedInterface;
-use Automattic\WooCommerce\Api\Infrastructure\ResolverHelpers;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class Widget {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Widget',
- 'description' => __( 'A dummy widget that exercises every output-type attribute', 'woocommerce' ),
- 'interfaces' => fn() => array(
- NamedInterface::get(),
- ),
- 'fields' => fn() => array(
- 'slug' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'A short slug', 'woocommerce' ),
- ),
- 'caption' => array(
- 'type' => Type::string(),
- 'description' => __( 'An optional caption', 'woocommerce' ),
- 'metadata' => array(
- 'visible_sample' => 'visible',
- ),
- 'authorization' => array(
- array(
- 'attribute' => 'RequiredCapability',
- 'args' => array(
- 0 => 'manage_woocommerce',
- ),
- ),
- ),
- 'resolve' => function ( $parent, $args, $context ) {
- $principal = $context['principal'];
- $_metadata = array(
- 'query' => $context['_query_metadata'] ?? array(),
- 'type' => array(),
- 'field' => array(
- 'visible_sample' => 'visible',
- ),
- );
- $_args = $args;
- $_parent = $parent;
- if ( ! ( ( new \Automattic\WooCommerce\Api\Attributes\RequiredCapability( 'manage_woocommerce' ) )->authorize( $principal ) ) ) {
- throw ResolverHelpers::build_field_authorization_error( $principal, 'Widget', 'caption', 'RequiredCapability' );
- }
- return $parent->caption;
- },
- ),
- 'color' => array(
- 'type' => Type::nonNull( ColorType::get() ),
- 'description' => __( 'The widget color', 'woocommerce' ),
- ),
- 'priority' => array(
- 'type' => Type::nonNull( PriorityType::get() ),
- 'description' => __( 'Priority assigned to this widget', 'woocommerce' ),
- ),
- 'tag_ids' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( Type::int() ) ) ),
- 'description' => __( 'Tag IDs assigned to this widget', 'woocommerce' ),
- ),
- 'featured_reviews' => array(
- 'type' => Type::nonNull( Type::listOf( Type::nonNull( WidgetReview::get() ) ) ),
- 'description' => __( 'Notable comments left on this widget', 'woocommerce' ),
- ),
- 'reviews' => array(
- 'type' => Type::nonNull( WidgetReviewConnectionType::get() ),
- 'description' => __( 'Reviews of the widget', 'woocommerce' ),
- ),
- 'date_created' => array(
- 'type' => DummyDateTimeType::get(),
- 'description' => __( 'When the widget was created', 'woocommerce' ),
- ),
- 'price' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The widget price', 'woocommerce' ),
- 'args' => array(
- 'formatted' => array(
- 'type' => Type::boolean(),
- 'defaultValue' => false,
- 'description' => __( 'When true, prepend a $ sign', 'woocommerce' ),
- ),
- ),
- ),
- 'legacy_price' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'A field flagged for removal', 'woocommerce' ),
- 'deprecationReason' => 'Use price instead.',
- ),
- 'label' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The display label for this object', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/WidgetReview.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/WidgetReview.php
deleted file mode 100644
index bd5e6c71b0d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Output/WidgetReview.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Interfaces\Identifiable as IdentifiableInterface;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetReview {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'WidgetReview',
- 'description' => __( 'A review left for a widget', 'woocommerce' ),
- 'interfaces' => fn() => array(
- IdentifiableInterface::get(),
- ),
- 'fields' => fn() => array(
- 'body' => array(
- 'type' => Type::nonNull( Type::string() ),
- 'description' => __( 'The body of the review', 'woocommerce' ),
- ),
- 'score' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'A score between 0 and 5', 'woocommerce' ),
- ),
- 'id' => array(
- 'type' => Type::nonNull( Type::int() ),
- 'description' => __( 'The unique numeric identifier', 'woocommerce' ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/PageInfo.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/PageInfo.php
deleted file mode 100644
index 3bfe0bb1ec5..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/PageInfo.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class PageInfo {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'PageInfo',
- 'fields' => array(
- 'has_next_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'has_previous_page' => array(
- 'type' => Type::nonNull( Type::boolean() ),
- ),
- 'start_cursor' => array(
- 'type' => Type::string(),
- ),
- 'end_cursor' => array(
- 'type' => Type::string(),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetConnection.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetConnection.php
deleted file mode 100644
index 22e9d82f9d0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'WidgetConnection',
- 'description' => __( 'A connection to a list of Widget items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- WidgetEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- WidgetType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetEdge.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetEdge.php
deleted file mode 100644
index 2c70f8311ca..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget as WidgetType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'WidgetEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( WidgetType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewConnection.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewConnection.php
deleted file mode 100644
index 324f62f6bd4..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewConnection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\WidgetReview as WidgetReviewType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetReviewConnection {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'WidgetReviewConnection',
- 'description' => __( 'A connection to a list of WidgetReview items.', 'woocommerce' ),
- 'fields' => fn() => array(
- 'edges' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- WidgetReviewEdge::get()
- )
- )
- ),
- ),
- 'nodes' => array(
- 'type' => Type::nonNull(
- Type::listOf(
- Type::nonNull(
- WidgetReviewType::get()
- )
- )
- ),
- ),
- 'page_info' => array(
- 'type' => Type::nonNull( PageInfo::get() ),
- ),
- 'total_count' => array(
- 'type' => Type::nonNull( Type::int() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewEdge.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewEdge.php
deleted file mode 100644
index 258fafb4fb0..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Pagination/WidgetReviewEdge.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Pagination;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\WidgetReview as WidgetReviewType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\Type;
-
-class WidgetReviewEdge {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'WidgetReviewEdge',
- 'fields' => fn() => array(
- 'cursor' => array(
- 'type' => Type::nonNull( Type::string() ),
- ),
- 'node' => array(
- 'type' => Type::nonNull( WidgetReviewType::get() ),
- ),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Scalars/DummyDateTime.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Scalars/DummyDateTime.php
deleted file mode 100644
index 52fbb64b11e..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/GraphQLTypes/Scalars/DummyDateTime.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Scalars;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Scalars\DummyDateTime as DummyDateTimeScalar;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\CustomScalarType;
-
-class DummyDateTime {
- private static ?CustomScalarType $instance = null;
-
- public static function get(): CustomScalarType {
- if ( null === self::$instance ) {
- self::$instance = new CustomScalarType(
- array(
- 'name' => 'DummyDateTime',
- 'description' => __( 'An ISO 8601 encoded date/time string used by the dummy API', 'woocommerce' ),
- 'serialize' => fn( $value ) => DummyDateTimeScalar::serialize( $value ),
- 'parseValue' => function ( $value ) {
- try {
- return DummyDateTimeScalar::parse( $value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- },
- 'parseLiteral' => function ( $value_node, ?array $variables = null ) {
- if ( $value_node instanceof \Automattic\WooCommerce\Api\Infrastructure\Schema\AST\StringValueNode ) {
- try {
- return DummyDateTimeScalar::parse( $value_node->value );
- } catch ( \InvalidArgumentException $e ) {
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error( $e->getMessage() );
- }
- }
- throw new \Automattic\WooCommerce\Api\Infrastructure\Schema\Error(
- 'DummyDateTime must be a string, got: ' . $value_node->kind
- );
- },
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootMutationType.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootMutationType.php
deleted file mode 100644
index 442384b392a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootMutationType.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations\Increment;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations\CreateWidget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations\PublicCreateWidget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLMutations\DeleteWidget;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootMutationType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Mutation',
- 'fields' => fn() => array(
- 'increment' => Increment::get_field_definition(),
- 'createWidget' => CreateWidget::get_field_definition(),
- 'publicCreateWidget' => PublicCreateWidget::get_field_definition(),
- 'deleteWidget' => DeleteWidget::get_field_definition(),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootQueryType.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootQueryType.php
deleted file mode 100644
index e27b1c8bd43..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/RootQueryType.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\GetIdentifiable;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\InheritedCapQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\InheritedFromInterfaceQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\OverriddenCapQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MergedCapsQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\InheritedPublicQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\GetWidget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\PublicWidgetAccess;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\AuthorizeThrowsQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\PrincipalAwareQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\ComposedAuthorizeQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MetadataAwareInternalQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\IgnoredAuthorizeQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\OverriddenAuthorizeQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\MetadataAwareNoFlagQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\AuthorizeOnlyQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\HiddenFlaggedQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\FailingQuery;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\ListWidgetsArray;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\ListWidgets;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLQueries\GetGreeting;
-use Automattic\WooCommerce\Api\Infrastructure\MetadataController;
-use Automattic\WooCommerce\Api\Infrastructure\Schema\ObjectType;
-
-class RootQueryType {
- private static ?ObjectType $instance = null;
-
- public static function get(): ObjectType {
- if ( null === self::$instance ) {
- self::$instance = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => fn() => array(
- 'namedThing' => GetIdentifiable::get_field_definition(),
- 'inheritedCap' => InheritedCapQuery::get_field_definition(),
- 'inheritedFromInterface' => InheritedFromInterfaceQuery::get_field_definition(),
- 'overriddenCap' => OverriddenCapQuery::get_field_definition(),
- 'mergedCaps' => MergedCapsQuery::get_field_definition(),
- 'inheritedPublic' => InheritedPublicQuery::get_field_definition(),
- 'widget' => GetWidget::get_field_definition(),
- 'publicWidget' => PublicWidgetAccess::get_field_definition(),
- 'authorizeThrows' => AuthorizeThrowsQuery::get_field_definition(),
- 'principalAware' => PrincipalAwareQuery::get_field_definition(),
- 'composedAuthorize' => ComposedAuthorizeQuery::get_field_definition(),
- 'metadataAwareInternalQuery' => MetadataAwareInternalQuery::get_field_definition(),
- 'ignoredAuthorize' => IgnoredAuthorizeQuery::get_field_definition(),
- 'overriddenAuthorize' => OverriddenAuthorizeQuery::get_field_definition(),
- 'metadataAwareNoFlagQuery' => MetadataAwareNoFlagQuery::get_field_definition(),
- 'authorizeOnly' => AuthorizeOnlyQuery::get_field_definition(),
- 'hiddenFlagged' => HiddenFlaggedQuery::get_field_definition(),
- 'failing' => FailingQuery::get_field_definition(),
- 'widgetList' => ListWidgetsArray::get_field_definition(),
- 'widgets' => ListWidgets::get_field_definition(),
- 'greeting' => GetGreeting::get_field_definition(),
- MetadataController::FIELD_NAME => MetadataController::get_field_definition(),
- ),
- )
- );
- }
- return self::$instance;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/TypeRegistry.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/TypeRegistry.php
deleted file mode 100644
index e4513c2a7d9..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/TypeRegistry.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-declare(strict_types=1);
-
-// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated;
-
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Gadget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\Widget;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLTypes\Output\WidgetReview;
-
-class TypeRegistry {
- /**
- * Return all concrete types that implement interfaces.
- *
- * Pass this to the Schema 'types' config so that inline fragments
- * (e.g. `... on VariableProduct`) are resolvable.
- *
- * @return array
- */
- public static function get_interface_implementors(): array {
- return array(
- Gadget::get(),
- Widget::get(),
- WidgetReview::get(),
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_generation_date.txt b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_generation_date.txt
deleted file mode 100644
index bc8bcfebc84..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_generation_date.txt
+++ /dev/null
@@ -1 +0,0 @@
-2026-05-21T11:44:02+00:00
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_source_hash.txt b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_source_hash.txt
deleted file mode 100644
index a2a92acab36..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/DummyApiAutogenerated/api_source_hash.txt
+++ /dev/null
@@ -1 +0,0 @@
-f18a5e93805642a14bacb2ad90ab3c7db3e1731a75187d032992eaa872f67533
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/Always200Resolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/Always200Resolver.php
deleted file mode 100644
index 9a8e3e7667a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/Always200Resolver.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that always returns 200 (Shopify-style override).
- */
-class Always200Resolver {
- /**
- * Always return 200 regardless of input.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $default_status, $output, $request );
- return 200;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/AlwaysThrowingResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/AlwaysThrowingResolver.php
deleted file mode 100644
index 2e46b045cd5..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/AlwaysThrowingResolver.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that throws on every call. Exercises the throw-safety path:
- * the framework must produce a clean 500 INTERNAL_ERROR without leaking the
- * exception message and without re-invoking the resolver.
- */
-class AlwaysThrowingResolver {
- /**
- * The exception message used by every throw — kept distinctive so tests
- * can assert it does NOT appear on the wire.
- */
- public const THROW_MESSAGE = 'resolver-implementation-detail';
-
- /**
- * Always throw a RuntimeException.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- *
- * @throws \RuntimeException Always.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $default_status, $output, $request );
- throw new \RuntimeException( self::THROW_MESSAGE );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/EagerCatchOnlyThrowingResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/EagerCatchOnlyThrowingResolver.php
deleted file mode 100644
index 187ea6e24d2..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/EagerCatchOnlyThrowingResolver.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that throws ONLY when handed the synthetic errors-shape
- * built by GraphQLController::handle_request()'s eager-catch block.
- * Exercises the no-loop guarantee: when the throw originates from inside
- * that catch, the framework must not re-invoke the resolver while building
- * the failure response.
- */
-class EagerCatchOnlyThrowingResolver {
- /**
- * Number of calls that returned successfully (i.e. did not match the
- * eager-catch heuristic).
- *
- * @var int
- */
- public int $calls_succeeded = 0;
-
- /**
- * Number of calls that matched the eager-catch heuristic and threw.
- *
- * @var int
- */
- public int $calls_thrown = 0;
-
- /**
- * Throw only on the synthetic eager-catch shape; pass through otherwise.
- *
- * Heuristic: a single error with no `data` key is exactly what
- * handle_request()'s catch block builds. Other decision points either
- * include `data` (decision #4) or are not currently exercised by this
- * fixture.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- *
- * @throws \RuntimeException When the input matches the eager-catch shape.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $request );
- $has_one_error = isset( $output['errors'] ) && 1 === count( $output['errors'] );
- $has_no_data = ! array_key_exists( 'data', $output );
- if ( $has_one_error && $has_no_data ) {
- ++$this->calls_thrown;
- throw new \RuntimeException( 'eager-catch-only' );
- }
- ++$this->calls_succeeded;
- return $default_status;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/FixedReturnResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/FixedReturnResolver.php
deleted file mode 100644
index 62a8ddee6cf..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/FixedReturnResolver.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that returns a fixed integer regardless of inputs. Used to
- * exercise pick_status()' range guard with values outside the 100..599 HTTP
- * range.
- */
-class FixedReturnResolver {
- /**
- * The integer this resolver always returns.
- *
- * @var int
- */
- private int $value;
-
- /**
- * Constructor.
- *
- * @param int $value The integer to return from every resolve_status() call.
- */
- public function __construct( int $value ) {
- $this->value = $value;
- }
-
- /**
- * Always return the configured value.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $default_status, $output, $request );
- return $this->value;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/PassThroughResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/PassThroughResolver.php
deleted file mode 100644
index b15c1ec3a9d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/PassThroughResolver.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that returns the framework default verbatim, recording each
- * call for later inspection.
- */
-class PassThroughResolver {
- /**
- * One entry per resolve_status() invocation, keyed `default` (the
- * framework-computed status passed in) and `codes` (the list of GraphQL
- * error codes seen in the output).
- *
- * @var array<int, array{default: int, codes: array<int, ?string>}>
- */
- public array $calls = array();
-
- /**
- * Pass through the framework default unchanged.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $request );
- $this->calls[] = array(
- 'default' => $default_status,
- 'codes' => array_map(
- static fn ( $err ) => $err['extensions']['code'] ?? null,
- $output['errors'] ?? array()
- ),
- );
- return $default_status;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/RemapInternalErrorResolver.php b/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/RemapInternalErrorResolver.php
deleted file mode 100644
index a1e873838ed..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/Fixtures/StatusResolvers/RemapInternalErrorResolver.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers;
-
-/**
- * Test resolver that remaps INTERNAL_ERROR responses to HTTP 503 and leaves
- * everything else on the framework default. Exercises partial-override
- * semantics.
- */
-class RemapInternalErrorResolver {
- /**
- * Remap INTERNAL_ERROR responses to 503; pass through everything else.
- *
- * @param int $default_status The framework-computed status.
- * @param array $output The response body about to be sent.
- * @param \WP_REST_Request $request The originating request.
- */
- public function resolve_status( int $default_status, array $output, \WP_REST_Request $request ): int {
- unset( $request );
- foreach ( $output['errors'] ?? array() as $error ) {
- if ( 'INTERNAL_ERROR' === ( $error['extensions']['code'] ?? null ) ) {
- return 503;
- }
- }
- return $default_status;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerDebugModeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerDebugModeTest.php
deleted file mode 100644
index 5b41fed5d48..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerDebugModeTest.php
+++ /dev/null
@@ -1,402 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLController as DummyGraphQLController;
-use WC_REST_Unit_Test_Case;
-
-/**
- * Tests for {@see GraphQLControllerBase}'s debug-mode surface: the
- * complexity / depth metrics surfaced under `extensions.debug`, the
- * previous-exception chain reporting, and the SerializationError →
- * BAD_USER_INPUT promotion path.
- */
-class GraphQLControllerDebugModeTest extends WC_REST_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var GraphQLControllerBase
- */
- private GraphQLControllerBase $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
-
- wp_cache_flush();
- DummyStore::reset();
- DummyContainer::reset();
-
- // Debug mode requires the caller to be an administrator (or a local
- // environment); make every test run as one.
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $this->sut = new DummyGraphQLController();
- $this->sut->init( new QueryCache() );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- wp_cache_flush();
- parent::tearDown();
- }
-
- /**
- * Build a POST request to /wc/graphql with the given body and an
- * optional `_debug=1` query-string trigger.
- *
- * @param array $body Request body params (query, variables, operationName, ...).
- * @param bool $debug When true, set `_debug=1` so handle_request enters debug mode.
- */
- private function post_request( array $body, bool $debug = false ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- foreach ( $body as $key => $value ) {
- $request->set_param( $key, $value );
- }
- if ( $debug ) {
- $request->set_query_params( array( '_debug' => '1' ) );
- }
- return $request;
- }
-
- /**
- * @testdox debug mode adds extensions.debug.complexity and extensions.debug.depth to successful responses.
- */
- public function test_debug_mode_emits_complexity_and_depth_metrics(): void {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayHasKey( 'extensions', $data );
- $this->assertArrayHasKey( 'debug', $data['extensions'] );
- $this->assertArrayHasKey( 'complexity', $data['extensions']['debug'] );
- $this->assertArrayHasKey( 'depth', $data['extensions']['debug'] );
- $this->assertIsInt( $data['extensions']['debug']['complexity'] );
-
- // Depth is reported as { tree_only, in_depth }: tree_only counts
- // only intermediate (non-leaf) levels, in_depth counts every field
- // in the deepest chain. `greeting { result }` has no nested
- // selection set under `greeting` and one leaf at depth 2.
- $depth = $data['extensions']['debug']['depth'];
- $this->assertIsArray( $depth );
- $this->assertArrayHasKey( 'tree_only', $depth );
- $this->assertArrayHasKey( 'in_depth', $depth );
- $this->assertSame( 0, $depth['tree_only'] );
- $this->assertSame( 2, $depth['in_depth'] );
- }
-
- /**
- * @testdox debug mode is off without _debug=1, even for an admin.
- */
- public function test_debug_mode_is_off_without_trigger(): void {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), false )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- // No debug metrics should be attached when _debug=1 is missing.
- $this->assertArrayNotHasKey( 'debug', $data['extensions'] ?? array() );
- }
-
- /**
- * @testdox debug mode honours operationName when computing the depth metric.
- */
- public function test_debug_mode_depth_metric_honours_operation_name(): void {
- $doc = '
- query Shallow { greeting { result } }
- query Deep { widget(id: 1) { id reviews { nodes { id body score } } } }
- ';
-
- $shallow = $this->sut->handle_request(
- $this->post_request(
- array(
- 'query' => $doc,
- 'operationName' => 'Shallow',
- ),
- true
- )
- );
- $deep = $this->sut->handle_request(
- $this->post_request(
- array(
- 'query' => $doc,
- 'operationName' => 'Deep',
- ),
- true
- )
- );
-
- // `in_depth` is what surfaces the deepest leaf chain — what users
- // actually care about when tuning the depth limit. `Shallow` has
- // just `greeting -> result` (depth 2); `Deep` has
- // `widget -> reviews -> nodes -> id` (depth 4).
- $this->assertSame( 2, $shallow->get_data()['extensions']['debug']['depth']['in_depth'] ?? null );
- $this->assertSame( 4, $deep->get_data()['extensions']['debug']['depth']['in_depth'] ?? null );
- }
-
- /**
- * @testdox debug mode surfaces the previous-exception chain for wrapped INTERNAL_ERRORs.
- */
- public function test_debug_mode_emits_previous_chain_for_wrapped_errors(): void {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ failing(kind: "runtime") { result } }' ), true )
- );
-
- $data = $response->get_data();
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
-
- $previous = $data['errors'][0]['extensions']['previous'] ?? null;
- $this->assertIsArray( $previous );
- $this->assertNotEmpty( $previous );
- // The wrapped RuntimeException's class and message must be visible
- // in the chain so a developer can see the real cause.
- $classes = array_column( $previous, 'class' );
- $messages = array_column( $previous, 'message' );
- $this->assertContains( \RuntimeException::class, $classes );
- $this->assertContains( 'Something blew up.', $messages );
- }
-
- /**
- * @testdox SerializationError on a returned value is promoted to BAD_USER_INPUT (HTTP 400).
- */
- public function test_serialization_error_is_promoted_to_bad_user_input(): void {
- // Increment returns the Int32-out-of-range result 2147483648 (== MAX_INT + 1),
- // which webonyx's IntType cannot serialize and surfaces as a
- // SerializationError. The controller's error formatter promotes it.
- $response = $this->sut->handle_request(
- $this->post_request(
- array( 'query' => 'mutation { increment(value: 2147483647, by: 1) { result } }' )
- )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'BAD_USER_INPUT', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox debug mode stays off for an authenticated low-privilege user even with `_debug=1`.
- */
- public function test_debug_mode_is_off_for_low_privilege_user_with_debug_param(): void {
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayNotHasKey( 'debug', $data['extensions'] ?? array() );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_use_debug_mode filter can grant debug mode to a user the principal would deny.
- */
- public function test_filter_can_grant_debug_mode_to_low_privilege_user(): void {
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $received_principal = null;
- $received_request = null;
- $filter = function ( bool $can_debug, ?object $principal, \WP_REST_Request $request ) use ( &$received_principal, &$received_request ): bool {
- $received_principal = $principal;
- $received_request = $request;
- return true;
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter, 10, 3 );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter, 10 );
- }
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayHasKey( 'debug', $data['extensions'] ?? array() );
-
- $this->assertNotNull( $received_principal, 'The filter should receive the resolved principal.' );
- $this->assertInstanceOf( \WP_REST_Request::class, $received_request );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_use_debug_mode filter can revoke debug mode from an admin.
- */
- public function test_filter_can_revoke_debug_mode_from_admin(): void {
- $filter = function (): bool {
- return false;
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
- }
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayNotHasKey( 'debug', $data['extensions'] ?? array() );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_use_debug_mode filter must return strictly true; truthy non-bool denies.
- */
- public function test_filter_requires_strict_true_to_grant_debug_mode(): void {
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $filter = function () {
- return 1;
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
- }
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayNotHasKey( 'debug', $data['extensions'] ?? array() );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_use_debug_mode filter is not invoked when _debug=1 is absent.
- */
- public function test_filter_is_not_invoked_without_debug_param(): void {
- $invoked = false;
- $filter = function ( bool $can_debug ) use ( &$invoked ): bool {
- $invoked = true;
- return $can_debug;
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
-
- try {
- $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), false )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
- }
-
- $this->assertFalse( $invoked );
- }
-
- /**
- * @testdox a throw from the woocommerce_graphql_can_use_debug_mode filter callback denies debug mode (fail-closed).
- *
- * Defensive: is_debug_mode() is invoked from format_exception() and
- * build_resolver_failure_response(), both of which run while the controller
- * is already handling another exception. A throw escaping is_debug_mode()
- * there would corrupt the error pipeline, so any throw must be caught.
- */
- public function test_filter_callback_throw_denies_debug_mode(): void {
- $filter = function () {
- throw new \RuntimeException( 'broken-filter-callback' );
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ greeting { result } }' ), true )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
- }
-
- // Response is well-formed (the throw didn't escape the controller),
- // and debug mode stayed off (the throw was treated as a deny).
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertArrayNotHasKey( 'debug', $data['extensions'] ?? array() );
-
- // The filter's exception message must not appear anywhere on the wire.
- $wire = wp_json_encode( $data );
- $this->assertIsString( $wire );
- $this->assertStringNotContainsString( 'broken-filter-callback', $wire );
- }
-
- /**
- * @testdox a null principal denies debug mode even when a permissive filter is installed.
- *
- * Honours the contract documented on handle_request(): when principal
- * resolution itself fails ($principal stays null), no debug info is
- * surfaced — the filter is not consulted in that case.
- */
- public function test_null_principal_denies_debug_mode_even_with_permissive_filter(): void {
- $filter_invoked = false;
- $filter = function ( bool $can_debug ) use ( &$filter_invoked ): bool {
- // Avoid parameter not used PHPCS errors.
- unset( $can_debug );
- $filter_invoked = true;
- return true;
- };
- add_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
-
- try {
- $reflection = new \ReflectionClass( GraphQLControllerBase::class );
- $method = $reflection->getMethod( 'is_debug_mode' );
- $method->setAccessible( true );
-
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- $request->set_query_params( array( '_debug' => '1' ) );
-
- $result = $method->invoke( $this->sut, null, $request );
- } finally {
- remove_filter( 'woocommerce_graphql_can_use_debug_mode', $filter );
- }
-
- $this->assertFalse( $result );
- $this->assertFalse( $filter_invoked, 'Filter must not be consulted for null principals.' );
- }
-
- /**
- * @testdox a throw from the principal's can_use_debug_mode() method denies debug mode (fail-closed).
- */
- public function test_principal_method_throw_denies_debug_mode(): void {
- $throwing_principal = new class() {
- /**
- * Always throw to simulate a buggy plugin principal.
- *
- * @throws \RuntimeException Always.
- */
- public function can_use_debug_mode(): bool {
- throw new \RuntimeException( 'broken-principal-method' );
- }
- };
-
- $reflection = new \ReflectionClass( GraphQLControllerBase::class );
- $method = $reflection->getMethod( 'is_debug_mode' );
- $method->setAccessible( true );
-
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- $request->set_query_params( array( '_debug' => '1' ) );
-
- $this->assertFalse( $method->invoke( $this->sut, $throwing_principal, $request ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerExecutionTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerExecutionTest.php
deleted file mode 100644
index 74f92945854..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerExecutionTest.php
+++ /dev/null
@@ -1,740 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\CountingNodeList;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLController as DummyGraphQLController;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use WC_REST_Unit_Test_Case;
-
-/**
- * End-to-end tests for {@see GraphQLControllerBase}, executing real requests
- * through a controller wired to the dummy fixture API.
- *
- * Covers the rules the controller enforces beyond what the schema itself
- * checks: depth limit, complexity limit, introspection gating, error
- * formatting, GET / POST routing, mutation rejection over GET, JSON
- * decoding of variables / extensions, and APQ.
- */
-class GraphQLControllerExecutionTest extends WC_REST_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var GraphQLControllerBase
- */
- private GraphQLControllerBase $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- wp_cache_flush();
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
-
- $this->sut = new DummyGraphQLController();
- $this->sut->init( new QueryCache() );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- wp_cache_flush();
- parent::tearDown();
- }
-
- /**
- * Build a POST WP_REST_Request with JSON-style body params.
- *
- * @param array $body The decoded request body.
- */
- private function post_request( array $body ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- foreach ( $body as $key => $value ) {
- $request->set_param( $key, $value );
- }
- return $request;
- }
-
- /**
- * Build a GET WP_REST_Request with query-string params.
- *
- * @param array $params The query-string parameters.
- */
- private function get_request( array $params ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'GET', '/wc/graphql' );
- foreach ( $params as $key => $value ) {
- $request->set_param( $key, $value );
- }
- return $request;
- }
-
- /**
- * @testdox handle_request returns 200 + data for a successful query.
- */
- public function test_handle_request_returns_200_for_successful_query(): void {
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
-
- $this->assertSame( 200, $response->get_status() );
- $data = $response->get_data();
- $this->assertSame( 'Hello, world!', $data['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox handle_request decodes JSON-encoded variables on GET requests.
- */
- public function test_handle_request_decodes_get_variables_json(): void {
- $response = $this->sut->handle_request(
- $this->get_request(
- array(
- 'query' => 'query Q($who: String) { greeting(name: $who) { result } }',
- 'variables' => '{"who":"Bob"}',
- )
- )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'Hello, Bob!', $response->get_data()['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox handle_request rejects malformed variables JSON with INVALID_ARGUMENT.
- */
- public function test_handle_request_rejects_malformed_variables_json(): void {
- $response = $this->sut->handle_request(
- $this->get_request(
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- )
- )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $data = $response->get_data();
- $this->assertSame( 'INVALID_ARGUMENT', $data['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request rejects non-object variables with INVALID_ARGUMENT.
- */
- public function test_handle_request_rejects_scalar_variables(): void {
- $response = $this->sut->handle_request(
- $this->get_request(
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => '"a string"',
- )
- )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'INVALID_ARGUMENT', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request rejects mutations over GET with METHOD_NOT_ALLOWED.
- */
- public function test_handle_request_rejects_mutation_over_get(): void {
- $response = $this->sut->handle_request(
- $this->get_request( array( 'query' => 'mutation { increment(value: 1) { result } }' ) )
- );
-
- $this->assertSame( 405, $response->get_status() );
- $this->assertSame( 'METHOD_NOT_ALLOWED', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request maps an authenticated-but-denied authorization to HTTP 403.
- */
- public function test_handle_request_maps_authorization_failure_to_403(): void {
- // `widget` requires manage_options. An editor is authenticated but lacks the
- // cap → attribute denies → FORBIDDEN/403 (the caller is recognised; re-auth
- // won't help).
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) ) );
-
- $this->assertSame( 403, $response->get_status() );
- $this->assertSame( 'FORBIDDEN', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request maps an anonymous-rejected authorization to HTTP 401.
- */
- public function test_handle_request_maps_anonymous_authorization_failure_to_401(): void {
- // `widget` requires manage_options. Anonymous caller → attribute denies but
- // the principal isn't authenticated → UNAUTHORIZED/401 (re-auth might help).
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) ) );
-
- $this->assertSame( 401, $response->get_status() );
- $this->assertSame( 'UNAUTHORIZED', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request rejects queries that exceed the configured depth limit.
- */
- public function test_handle_request_rejects_excessive_depth(): void {
- // DEFAULT_MAX_QUERY_DEPTH is 15. Build a deeply nested query well past that.
- $inner = 'id';
- for ( $i = 0; $i < 20; $i++ ) {
- $inner = "reviews { nodes { $inner } }";
- }
- $query = "{ widget(id: 1) { $inner } }";
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => $query ) ) );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertNotEmpty( $response->get_data()['errors'] ?? array() );
- }
-
- /**
- * @testdox handle_request rejects queries that exceed the configured complexity limit.
- */
- public function test_handle_request_rejects_excessive_complexity(): void {
- // Complexity for the widgets connection field with first=100 and a
- // child of complexity ≥ 11 will exceed DEFAULT_MAX_QUERY_COMPLEXITY (1000).
- $query = '{
- widgets(first: 100) {
- nodes {
- reviews(first: 100) {
- nodes { id body score }
- }
- }
- }
- }';
-
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => $query ) ) );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertNotEmpty( $response->get_data()['errors'] ?? array() );
- }
-
- /**
- * Build a document in which each named fragment spreads the next one twice,
- * so the number of spreads reachable from the root doubles with every fragment.
- *
- * @param int $fragment_count Number of chained fragments.
- * @param string $type_name Type condition of the fragments.
- * @param string $root Selection set body of the operation (must spread F0).
- * @param string $leaf Selection set body of the last fragment.
- */
- private function build_duplicate_spread_chain( int $fragment_count, string $type_name, string $root, string $leaf ): string {
- $document = "query Q { {$root} }\n";
- for ( $i = 0; $i < $fragment_count - 1; $i++ ) {
- $next = $i + 1;
- $document .= "fragment F{$i} on {$type_name} { ...F{$next} ...F{$next} }\n";
- }
- $last = $fragment_count - 1;
-
- return $document . "fragment F{$last} on {$type_name} { {$leaf} }\n";
- }
-
- /**
- * Upper bound on how many times the request pipeline (validation rules,
- * executor, query info extraction) may iterate one fragment's selections:
- * a handful of passes that each visit a fragment once (4 when validation
- * rejects the document and 6 when it executes, at the time of writing),
- * as opposed to once per spread.
- */
- private const MAX_ITERATIONS_PER_FRAGMENT = 32;
-
- /**
- * Make the controller process a pre-parsed document whose fragment
- * selection sets count how often they are iterated.
- *
- * Injected through a QueryCache double so the AST reaches the controller
- * as-is (the real cache would re-parse the query string). Resets the
- * iteration counter.
- *
- * @param string $query The GraphQL document.
- * @return int The number of fragments in the document.
- */
- private function inject_counting_document( string $query ): int {
- $document = Parser::parse( $query, array( 'noLocation' => true ) );
- $fragment_count = CountingNodeList::instrument_fragments( $document );
- CountingNodeList::reset();
-
- $cache = new class( $document ) extends QueryCache {
- /**
- * Constructor.
- *
- * @param DocumentNode $document The document to hand to the controller.
- */
- public function __construct( private DocumentNode $document ) {}
-
- /**
- * Return the injected document regardless of the request.
- *
- * @param ?string $query Ignored.
- * @param array $extensions Ignored.
- */
- public function resolve( ?string $query, array $extensions ): DocumentNode {
- unset( $query, $extensions );
- return $this->document;
- }
- };
- $this->sut->init( $cache );
-
- return $fragment_count;
- }
-
- /**
- * @testdox handle_request rejects a document whose fragments spread each other twice, visiting each fragment a bounded number of times.
- */
- public function test_handle_request_rejects_duplicate_spread_document_with_bounded_work(): void {
- $query = $this->build_duplicate_spread_chain( 24, 'Query', '...F0', '__typename' );
- $fragment_count = $this->inject_counting_document( $query );
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => $query ) ) );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'Maximum query complexity exceeded.', $response->get_data()['errors'][0]['message'] ?? null );
- $this->assertLessThanOrEqual( self::MAX_ITERATIONS_PER_FRAGMENT * $fragment_count, CountingNodeList::$iterations );
- }
-
- /**
- * @testdox handle_request executes a zero-complexity document whose fragments spread each other twice, visiting each fragment a bounded number of times in the resolvers.
- */
- public function test_handle_request_executes_zero_complexity_duplicate_spread_document_with_bounded_work(): void {
- // `first: 0` keeps the score within the limit, so the document reaches
- // the resolver and its QueryInfoExtractor call, which must also expand
- // each fragment only once.
- $query = $this->build_duplicate_spread_chain( 24, 'WidgetConnection', 'widgets(first: 0) { ...F0 }', '__typename' );
- $fragment_count = $this->inject_counting_document( $query );
-
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => $query ) ) );
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'WidgetConnection', $response->get_data()['data']['widgets']['__typename'] ?? null );
- $this->assertLessThanOrEqual( self::MAX_ITERATIONS_PER_FRAGMENT * $fragment_count, CountingNodeList::$iterations );
- }
-
- /**
- * @testdox handle_request blocks introspection for low-privilege callers.
- */
- public function test_handle_request_blocks_introspection_for_unauthorized_users(): void {
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
-
- $data = $response->get_data();
- // Either errors out or returns no data — depending on validation rule.
- $this->assertNotEmpty( $data['errors'] ?? array() );
- }
-
- /**
- * @testdox handle_request allows introspection for users with manage_woocommerce.
- */
- public function test_handle_request_allows_introspection_for_admins(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'Query', $response->get_data()['data']['__schema']['queryType']['name'] ?? null );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_introspect filter can grant introspection to an anonymous request.
- */
- public function test_filter_can_grant_introspection_to_anonymous_user(): void {
- wp_set_current_user( 0 );
-
- $received_principal = null;
- $received_request = null;
- $filter = function ( bool $can_introspect, ?object $principal, \WP_REST_Request $request ) use ( &$received_principal, &$received_request ): bool {
- $received_principal = $principal;
- $received_request = $request;
- return true;
- };
- add_filter( 'woocommerce_graphql_can_introspect', $filter, 10, 3 );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_introspect', $filter, 10 );
- }
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'Query', $response->get_data()['data']['__schema']['queryType']['name'] ?? null );
-
- $this->assertNotNull( $received_principal, 'The filter should receive the resolved principal even for anonymous requests.' );
- $this->assertInstanceOf( \WP_REST_Request::class, $received_request );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_introspect filter can revoke introspection from an admin.
- */
- public function test_filter_can_revoke_introspection_from_admin(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $filter = function (): bool {
- return false;
- };
- add_filter( 'woocommerce_graphql_can_introspect', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_introspect', $filter );
- }
-
- $data = $response->get_data();
- $this->assertNotEmpty( $data['errors'] ?? array() );
- }
-
- /**
- * @testdox the woocommerce_graphql_can_introspect filter must return strictly true; truthy non-bool denies.
- */
- public function test_filter_requires_strict_true_to_grant_introspection(): void {
- wp_set_current_user( 0 );
-
- $filter = function () {
- return 1;
- };
- add_filter( 'woocommerce_graphql_can_introspect', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_introspect', $filter );
- }
-
- $data = $response->get_data();
- $this->assertNotEmpty( $data['errors'] ?? array() );
- }
-
- /**
- * @testdox handle_request returns a syntax error as HTTP 400 GRAPHQL_PARSE_ERROR.
- */
- public function test_handle_request_handles_syntax_error(): void {
- $response = $this->sut->handle_request( $this->post_request( array( 'query' => '{ widget(id:' ) ) );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'GRAPHQL_PARSE_ERROR', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request returns BAD_REQUEST when the request has no query and no APQ.
- */
- public function test_handle_request_rejects_missing_query(): void {
- $response = $this->sut->handle_request( $this->post_request( array() ) );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'BAD_REQUEST', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request executes a previously-registered APQ via hash-only follow-up.
- */
- public function test_handle_request_apq_round_trip(): void {
- $query = '{ greeting { result } }';
- $hash = hash( 'sha256', $query );
-
- $register = $this->sut->handle_request(
- $this->post_request(
- array(
- 'query' => $query,
- 'extensions' => array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => $hash,
- ),
- ),
- )
- )
- );
- $this->assertSame( 200, $register->get_status() );
-
- $followup = $this->sut->handle_request(
- $this->post_request(
- array(
- 'extensions' => array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => $hash,
- ),
- ),
- )
- )
- );
-
- $this->assertSame( 200, $followup->get_status() );
- $this->assertSame( 'Hello, world!', $followup->get_data()['data']['greeting']['result'] ?? null );
- }
-
- /**
- * @testdox handle_request returns PERSISTED_QUERY_NOT_FOUND on HTTP 200 for unknown hashes.
- */
- public function test_handle_request_apq_unknown_hash_returns_200(): void {
- $response = $this->sut->handle_request(
- $this->post_request(
- array(
- 'extensions' => array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => str_repeat( '0', 64 ),
- ),
- ),
- )
- )
- );
-
- // APQ hash misses are intentionally HTTP 200 per the Apollo protocol.
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'PERSISTED_QUERY_NOT_FOUND', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request falls back to HTTP 500 for resolver errors with no entry in the controller's status map.
- */
- public function test_handle_request_translates_failing_query_errors(): void {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ failing(kind: "api_exception") { result } }' ) )
- );
-
- // CUSTOM_FAILURE has no entry in the controller's status map, so it
- // falls back to 500.
- $this->assertSame( 500, $response->get_status() );
- $this->assertSame( 'CUSTOM_FAILURE', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request honours operationName when deciding whether to reject a mutation over GET.
- */
- public function test_handle_request_uses_operation_name_for_mutation_check(): void {
- $multi_op = 'query GetIt { greeting { result } } mutation DoIt { increment(value: 1) { result } }';
-
- // Picking the query operation must NOT trip the mutation-over-GET guard.
- $query_response = $this->sut->handle_request(
- $this->get_request(
- array(
- 'query' => $multi_op,
- 'operationName' => 'GetIt',
- )
- )
- );
- $this->assertSame( 200, $query_response->get_status() );
- $this->assertSame( 'Hello, world!', $query_response->get_data()['data']['greeting']['result'] ?? null );
-
- // Picking the mutation operation in the same document MUST be rejected.
- $mutation_response = $this->sut->handle_request(
- $this->get_request(
- array(
- 'query' => $multi_op,
- 'operationName' => 'DoIt',
- )
- )
- );
- $this->assertSame( 405, $mutation_response->get_status() );
- $this->assertSame(
- 'METHOD_NOT_ALLOWED',
- $mutation_response->get_data()['errors'][0]['extensions']['code'] ?? null
- );
- }
-
- /**
- * @testdox a wrong-type field argument is rejected as BAD_USER_INPUT (HTTP 400).
- */
- public function test_handle_request_field_arg_type_mismatch_is_bad_user_input(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ widget(id: "not-an-int") { id } }' ) )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'BAD_USER_INPUT', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a missing required field argument is rejected as BAD_USER_INPUT (HTTP 400).
- */
- public function test_handle_request_missing_required_arg_is_bad_user_input(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ widget { id } }' ) )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'BAD_USER_INPUT', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a variable whose value doesn't match its declared type is rejected as BAD_USER_INPUT.
- */
- public function test_handle_request_variable_type_mismatch_is_bad_user_input(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $response = $this->sut->handle_request(
- $this->post_request(
- array(
- 'query' => 'query Q($id: Int!) { widget(id: $id) { id } }',
- 'variables' => array( 'id' => 'not-an-int' ),
- )
- )
- );
-
- $this->assertSame( 400, $response->get_status() );
- $this->assertSame( 'BAD_USER_INPUT', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox handle_request resolves the request principal eagerly and threads it into resolvers.
- *
- * The dummy fixture's principalAware query echoes the principal user_login.
- * Because no PrincipalResolver convention class is shipped under the fixture,
- * the controller falls back to a {@see \Automattic\WooCommerce\Api\Infrastructure\Principal}
- * wrapping `wp_get_current_user()` — the same default WC core's own endpoint uses.
- */
- public function test_handle_request_threads_principal_into_resolver(): void {
- $user_id = self::factory()->user->create(
- array(
- 'user_login' => 'carol',
- 'role' => 'subscriber',
- )
- );
- wp_set_current_user( $user_id );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ principalAware { result } }' ) )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'carol', $response->get_data()['data']['principalAware']['result'] ?? null );
- }
-
- /**
- * @testdox handle_request threads an anonymous-marker principal for unauthenticated requests.
- */
- public function test_handle_request_threads_anonymous_principal(): void {
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ principalAware { result } }' ) )
- );
-
- $this->assertSame( 200, $response->get_status() );
- $this->assertSame( 'anonymous', $response->get_data()['data']['principalAware']['result'] ?? null );
- }
-
- /**
- * @testdox a throw from the woocommerce_graphql_can_introspect filter callback denies introspection (fail-closed).
- */
- public function test_filter_callback_throw_denies_introspection(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $filter = function () {
- throw new \RuntimeException( 'broken-filter-callback' );
- };
- add_filter( 'woocommerce_graphql_can_introspect', $filter );
-
- try {
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
- } finally {
- remove_filter( 'woocommerce_graphql_can_introspect', $filter );
- }
-
- // Validation rejects the introspection query (fail-closed), and the
- // filter's exception message must not appear anywhere on the wire.
- $data = $response->get_data();
- $this->assertNotEmpty( $data['errors'] ?? array() );
-
- $wire = wp_json_encode( $data );
- $this->assertIsString( $wire );
- $this->assertStringNotContainsString( 'broken-filter-callback', $wire );
- }
-
- /**
- * @testdox a null principal denies introspection even when a permissive filter is installed.
- */
- public function test_null_principal_denies_introspection_even_with_permissive_filter(): void {
- $filter_invoked = false;
- $filter = function ( bool $can_introspect ) use ( &$filter_invoked ): bool {
- // Avoid parameter not used PHPCS errors.
- unset( $can_introspect );
- $filter_invoked = true;
- return true;
- };
- add_filter( 'woocommerce_graphql_can_introspect', $filter );
-
- try {
- $reflection = new \ReflectionClass( GraphQLControllerBase::class );
- $method = $reflection->getMethod( 'is_introspection_allowed' );
- $method->setAccessible( true );
-
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- $result = $method->invoke( $this->sut, null, $request );
- } finally {
- remove_filter( 'woocommerce_graphql_can_introspect', $filter );
- }
-
- $this->assertFalse( $result );
- $this->assertFalse( $filter_invoked, 'Filter must not be consulted for null principals.' );
- }
-
- /**
- * @testdox a throw from the principal's can_introspect() method denies introspection (fail-closed).
- */
- public function test_principal_method_throw_denies_introspection(): void {
- $throwing_principal = new class() {
- /**
- * Always throw to simulate a buggy plugin principal.
- *
- * @throws \RuntimeException Always.
- */
- public function can_introspect(): bool {
- throw new \RuntimeException( 'broken-principal-method' );
- }
- };
-
- $reflection = new \ReflectionClass( GraphQLControllerBase::class );
- $method = $reflection->getMethod( 'is_introspection_allowed' );
- $method->setAccessible( true );
-
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- $this->assertFalse( $method->invoke( $this->sut, $throwing_principal, $request ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerStatusResolverTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerStatusResolverTest.php
deleted file mode 100644
index e301fd22e1e..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerStatusResolverTest.php
+++ /dev/null
@@ -1,580 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLController as DummyGraphQLController;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\Always200Resolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\AlwaysThrowingResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\EagerCatchOnlyThrowingResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\FixedReturnResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\PassThroughResolver;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\StatusResolvers\RemapInternalErrorResolver;
-use WC_REST_Unit_Test_Case;
-
-/**
- * End-to-end tests covering the optional plugin-supplied HTTP status
- * resolver: per-decision-point routing, partial overrides, the Shopify-style
- * "always 200" override, and throw-safety.
- *
- * Test resolvers are wired into the controller via an anonymous subclass of
- * the dummy autogenerated controller that overrides
- * {@see GraphQLControllerBase::get_status_resolver()}. That mirrors the path
- * ApiBuilder takes when emitting the autogenerated subclass for a plugin
- * that ships an `Infrastructure\HttpStatusResolver`.
- */
-class GraphQLControllerStatusResolverTest extends WC_REST_Unit_Test_Case {
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- wp_cache_flush();
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- wp_cache_flush();
- parent::tearDown();
- }
-
- /**
- * Build a controller wired to the dummy fixture API and the supplied
- * status resolver.
- *
- * @param ?object $resolver Status resolver to inject (or null for none).
- */
- private function controller_with_resolver( ?object $resolver ): GraphQLControllerBase {
- $controller = new class( $resolver ) extends DummyGraphQLController {
- /**
- * The status resolver injected for this test, or null.
- *
- * @var ?object
- */
- private ?object $injected_resolver;
-
- /**
- * Constructor.
- *
- * @param ?object $injected_resolver The resolver to expose via get_status_resolver().
- */
- public function __construct( ?object $injected_resolver ) {
- $this->injected_resolver = $injected_resolver;
- }
-
- /**
- * Return the test-injected resolver instead of the (absent) plugin default.
- */
- protected function get_status_resolver(): ?object {
- return $this->injected_resolver;
- }
- };
- $controller->init( new QueryCache() );
- return $controller;
- }
-
- /**
- * Build a POST WP_REST_Request with JSON-style body params.
- *
- * @param array $body The decoded request body.
- */
- private function post_request( array $body ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- foreach ( $body as $key => $value ) {
- $request->set_param( $key, $value );
- }
- return $request;
- }
-
- /**
- * Build a GET WP_REST_Request with query-string params.
- *
- * @param array $params The query-string parameters.
- */
- private function get_request( array $params ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'GET', '/wc/graphql' );
- foreach ( $params as $key => $value ) {
- $request->set_param( $key, $value );
- }
- return $request;
- }
-
- // ------------------------------------------------------------------
- // No-op / pass-through resolver: defaults must be unchanged.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A pass-through resolver leaves the framework defaults unchanged across every decision point.
- */
- public function test_pass_through_resolver_preserves_defaults(): void {
- $resolver = new PassThroughResolver();
- $sut = $this->controller_with_resolver( $resolver );
-
- // Decision point #4: success path.
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
- $this->assertSame( 200, $response->get_status() );
-
- // Decision point #4: UNAUTHORIZED path (widget query without caps).
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) ) );
- $this->assertSame( 401, $response->get_status() );
- $this->assertSame( 'UNAUTHORIZED', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
-
- // Decision point #2: query cache resolve error (missing query).
- $response = $sut->handle_request( $this->post_request( array() ) );
- $this->assertSame( 400, $response->get_status() );
-
- // Decision point #2: APQ hash miss → 200 PERSISTED_QUERY_NOT_FOUND.
- $response = $sut->handle_request(
- $this->post_request(
- array(
- 'extensions' => array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => str_repeat( '0', 64 ),
- ),
- ),
- )
- )
- );
- $this->assertSame( 200, $response->get_status() );
-
- // Decision point #3: mutation over GET → 405.
- $response = $sut->handle_request(
- $this->get_request( array( 'query' => 'mutation { increment(value: 1) { result } }' ) )
- );
- $this->assertSame( 405, $response->get_status() );
-
- // Decision point #4 (final return): unhandled RuntimeException is
- // caught inside ResolverHelpers::translate_exceptions and surfaces as
- // INTERNAL_ERROR → 500.
- $response = $sut->handle_request(
- $this->post_request( array( 'query' => '{ failing(kind: "runtime") { result } }' ) )
- );
- $this->assertSame( 500, $response->get_status() );
-
- // Decision point #1 (eager-catch): an exception that escapes
- // process_request entirely. Malformed variables JSON throws
- // \InvalidArgumentException out of decode_json_param, which lands
- // in handle_request's catch and formats as INVALID_ARGUMENT → 400.
- $response = $sut->handle_request(
- $this->get_request(
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- )
- )
- );
- $this->assertSame( 400, $response->get_status() );
-
- // Resolver was consulted at every decision point.
- $this->assertGreaterThanOrEqual( 7, count( $resolver->calls ) );
- // And every call received the framework's pre-resolver default as the first argument.
- foreach ( $resolver->calls as $call ) {
- $this->assertIsInt( $call['default'] );
- }
- }
-
- // ------------------------------------------------------------------
- // Shopify-style: always 200.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A "always 200" resolver overrides every status across every decision point while leaving the body untouched.
- *
- * @dataProvider provider_always_200_cases
- *
- * @param string $method HTTP method to use.
- * @param array $payload Request payload.
- * @param ?string $expected_error_code Expected error code on the wire (null = no errors).
- * @param int $default_status_without_resolver Status the framework would have returned without a resolver.
- */
- public function test_always_200_resolver_overrides_every_decision_point(
- string $method,
- array $payload,
- ?string $expected_error_code,
- int $default_status_without_resolver
- ): void {
- $sut = $this->controller_with_resolver( new Always200Resolver() );
-
- $request = 'POST' === $method ? $this->post_request( $payload ) : $this->get_request( $payload );
- $response = $sut->handle_request( $request );
-
- $this->assertSame( 200, $response->get_status() );
-
- if ( null === $expected_error_code ) {
- $this->assertEmpty( $response->get_data()['errors'] ?? array() );
- } else {
- $this->assertSame( $expected_error_code, $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- // Sanity-check: without the resolver, the default would be different
- // (skipping the cases where the default is already 200).
- if ( 200 !== $default_status_without_resolver ) {
- $baseline = $this->controller_with_resolver( null );
- $baseline_response = $baseline->handle_request(
- 'POST' === $method ? $this->post_request( $payload ) : $this->get_request( $payload )
- );
- $this->assertSame( $default_status_without_resolver, $baseline_response->get_status() );
- }
- }
-
- /**
- * Data provider for {@see self::test_always_200_resolver_overrides_every_decision_point()}.
- *
- * @return array<string, array{0: string, 1: array, 2: ?string, 3: int}>
- */
- public function provider_always_200_cases(): array {
- return array(
- 'success (decision #4, no errors)' => array( 'POST', array( 'query' => '{ greeting { result } }' ), null, 200 ),
- 'unauthorized (decision #4)' => array( 'POST', array( 'query' => '{ widget(id: 1) { id } }' ), 'UNAUTHORIZED', 401 ),
- 'parse error (decision #2)' => array( 'POST', array( 'query' => '{ widget(id:' ), 'GRAPHQL_PARSE_ERROR', 400 ),
- 'missing query (decision #2)' => array( 'POST', array(), 'BAD_REQUEST', 400 ),
- 'mutation over GET (decision #3)' => array(
- 'GET',
- array( 'query' => 'mutation { increment(value: 1) { result } }' ),
- 'METHOD_NOT_ALLOWED',
- 405,
- ),
- 'unhandled runtime exception (decision #4)' => array(
- 'POST',
- array( 'query' => '{ failing(kind: "runtime") { result } }' ),
- 'INTERNAL_ERROR',
- 500,
- ),
- 'malformed variables JSON (decision #1, eager-catch)' => array(
- 'GET',
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- ),
- 'INVALID_ARGUMENT',
- 400,
- ),
- );
- }
-
- // ------------------------------------------------------------------
- // Partial override: remap one code, defer otherwise.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A resolver that remaps INTERNAL_ERROR to 503 only changes that response and leaves everything else on the default.
- */
- public function test_partial_override_for_one_code(): void {
- $sut = $this->controller_with_resolver( new RemapInternalErrorResolver() );
-
- // Internal error → remapped to 503.
- $response = $sut->handle_request(
- $this->post_request( array( 'query' => '{ failing(kind: "runtime") { result } }' ) )
- );
- $this->assertSame( 503, $response->get_status() );
- $this->assertSame( 'INTERNAL_ERROR', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
-
- // Unauthorized → unchanged at 401.
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) ) );
- $this->assertSame( 401, $response->get_status() );
-
- // Parse error → unchanged at 400.
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ widget(id:' ) ) );
- $this->assertSame( 400, $response->get_status() );
-
- // Success → unchanged at 200.
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
- $this->assertSame( 200, $response->get_status() );
- }
-
- // ------------------------------------------------------------------
- // Throw-safety: any throw produces a clean 500 INTERNAL_ERROR.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A resolver that throws on every call always produces a 500 INTERNAL_ERROR with the canonical body shape.
- *
- * @dataProvider provider_throw_safety_inputs
- *
- * @param string $method HTTP method to use.
- * @param array $payload Request payload.
- */
- public function test_throwing_resolver_produces_clean_500( string $method, array $payload ): void {
- $sut = $this->controller_with_resolver( new AlwaysThrowingResolver() );
-
- $request = 'POST' === $method ? $this->post_request( $payload ) : $this->get_request( $payload );
- $response = $sut->handle_request( $request );
-
- $this->assertSame( 500, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'An unexpected error occurred.', $data['errors'][0]['message'] ?? null );
-
- // Resolver's exception message must NOT leak onto the wire.
- $wire = wp_json_encode( $data );
- $this->assertIsString( $wire );
- $this->assertStringNotContainsString( AlwaysThrowingResolver::THROW_MESSAGE, $wire );
- }
-
- /**
- * Data provider for {@see self::test_throwing_resolver_produces_clean_500()}.
- * One input per status-decision point in the controller.
- *
- * @return array<string, array{0: string, 1: array}>
- */
- public function provider_throw_safety_inputs(): array {
- return array(
- '#1 eager-catch (malformed variables JSON throws out of process_request)' => array(
- 'GET',
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- ),
- ),
- '#2 query-cache resolve error (missing query)' => array( 'POST', array() ),
- '#3 mutation over GET' => array(
- 'GET',
- array( 'query' => 'mutation { increment(value: 1) { result } }' ),
- ),
- '#4 success path' => array(
- 'POST',
- array( 'query' => '{ greeting { result } }' ),
- ),
- '#4 unauthorized path' => array(
- 'POST',
- array( 'query' => '{ widget(id: 1) { id } }' ),
- ),
- );
- }
-
- /**
- * @testdox A resolver that throws ONLY when handed the eager-catch synthetic errors shape still terminates cleanly with a 500.
- *
- * Defensive: this is the configuration where the throw originates from
- * inside handle_request()'s own catch block. The framework must not loop
- * back into the resolver while building the failure response.
- */
- public function test_resolver_throwing_only_on_eager_catch_does_not_loop(): void {
- $resolver = new EagerCatchOnlyThrowingResolver();
- $sut = $this->controller_with_resolver( $resolver );
-
- // Trigger an exception that escapes process_request (malformed
- // variables JSON). handle_request()'s catch builds the synthetic
- // errors shape and calls pick_status — which throws via this
- // resolver's heuristic.
- $response = $sut->handle_request(
- $this->get_request(
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- )
- )
- );
-
- $this->assertSame( 500, $response->get_status() );
- $this->assertSame( 'INTERNAL_ERROR', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
-
- // The resolver was consulted exactly once (no infinite loop, no
- // post-failure re-entry).
- $this->assertSame( 1, $resolver->calls_thrown );
- $this->assertSame( 0, $resolver->calls_succeeded );
- }
-
- // ------------------------------------------------------------------
- // Out-of-range return: same clean 500 INTERNAL_ERROR as a throw.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A resolver that returns an out-of-range int produces a clean 500 INTERNAL_ERROR.
- *
- * @dataProvider provider_out_of_range_values
- *
- * @param int $value Out-of-range integer to return from the resolver.
- */
- public function test_out_of_range_resolver_return_produces_clean_500( int $value ): void {
- $sut = $this->controller_with_resolver( new FixedReturnResolver( $value ) );
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
-
- $this->assertSame( 500, $response->get_status() );
- $this->assertSame( 'INTERNAL_ERROR', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'An unexpected error occurred.', $response->get_data()['errors'][0]['message'] ?? null );
- }
-
- /**
- * Data provider for {@see self::test_out_of_range_resolver_return_produces_clean_500()}.
- * Covers the four "obviously invalid" buckets: zero, negative, sub-100, and above-599.
- *
- * @return array<string, array{0: int}>
- */
- public function provider_out_of_range_values(): array {
- return array(
- 'zero' => array( 0 ),
- 'negative' => array( -1 ),
- 'just below 100' => array( 99 ),
- 'just above 599' => array( 600 ),
- 'huge positive number' => array( 99999 ),
- );
- }
-
- /**
- * @testdox A resolver that returns an in-range int (100..599) is honoured even for non-IANA codes.
- *
- * The controller's range guard is intentionally permissive — it only
- * rejects nonsensical values, not arbitrary "unknown" codes. WordPress's
- * own `status_header()` will silently downgrade non-IANA codes to 200 at
- * the transport layer, but that is outside the resolver pipeline; this
- * test asserts the framework hands the resolved value through unchanged.
- */
- public function test_in_range_but_non_iana_value_is_honoured_by_pick_status(): void {
- $sut = $this->controller_with_resolver( new FixedReturnResolver( 222 ) );
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
-
- // Note: WP_REST_Response::get_status() returns whatever the controller
- // stored, before WP's status_header() lookup-table filter — so 222
- // shows up here even though a real HTTP client would observe 200.
- $this->assertSame( 222, $response->get_status() );
- }
-
- // ------------------------------------------------------------------
- // No resolver: behaviour is unchanged from before this PR.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A controller without a resolver behaves exactly as the framework defaults dictate.
- */
- public function test_no_resolver_preserves_defaults(): void {
- $sut = $this->controller_with_resolver( null );
-
- $this->assertSame(
- 200,
- $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) )->get_status()
- );
- $this->assertSame(
- 401,
- $sut->handle_request( $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) ) )->get_status()
- );
- $this->assertSame(
- 405,
- $sut->handle_request( $this->get_request( array( 'query' => 'mutation { increment(value: 1) { result } }' ) ) )->get_status()
- );
- $this->assertSame(
- 500,
- $sut->handle_request( $this->post_request( array( 'query' => '{ failing(kind: "runtime") { result } }' ) ) )->get_status()
- );
- $this->assertSame(
- 400,
- $sut->handle_request(
- $this->get_request(
- array(
- 'query' => '{ greeting { result } }',
- 'variables' => 'not-json',
- )
- )
- )->get_status()
- );
- }
-
- // ------------------------------------------------------------------
- // Debug-mode enrichment for the canonical 500: extensions.debug
- // surfaces the wrapper exception, and extensions.previous chains
- // through to the resolver's own throw when there was one.
- // ------------------------------------------------------------------
-
- /**
- * @testdox A throwing resolver in debug mode surfaces the wrapper message and the resolver's previous chain.
- */
- public function test_throwing_resolver_in_debug_mode_surfaces_previous_chain(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $sut = $this->controller_with_resolver( new AlwaysThrowingResolver() );
- $request = $this->post_request( array( 'query' => '{ greeting { result } }' ) );
- $request->set_query_params( array( '_debug' => '1' ) );
-
- $response = $sut->handle_request( $request );
- $this->assertSame( 500, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'An unexpected error occurred.', $data['errors'][0]['message'] ?? null );
-
- $debug = $data['errors'][0]['extensions']['debug'] ?? null;
- $this->assertIsArray( $debug );
- $this->assertSame( 'HTTP status resolver threw.', $debug['message'] ?? null );
- $this->assertArrayHasKey( 'file', $debug );
- $this->assertArrayHasKey( 'line', $debug );
- $this->assertArrayHasKey( 'trace', $debug );
-
- // The previous chain must contain the resolver's own RuntimeException
- // with its distinctive message — that's the actual cause a developer
- // needs to see.
- $previous = $data['errors'][0]['extensions']['previous'] ?? null;
- $this->assertIsArray( $previous );
- $this->assertContains( \RuntimeException::class, array_column( $previous, 'class' ) );
- $this->assertContains( AlwaysThrowingResolver::THROW_MESSAGE, array_column( $previous, 'message' ) );
- }
-
- /**
- * @testdox An out-of-range resolver return in debug mode surfaces the wrapper message; no previous chain (the wrapper had no cause).
- */
- public function test_out_of_range_resolver_return_in_debug_mode_surfaces_wrapper_message(): void {
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $sut = $this->controller_with_resolver( new FixedReturnResolver( 0 ) );
- $request = $this->post_request( array( 'query' => '{ greeting { result } }' ) );
- $request->set_query_params( array( '_debug' => '1' ) );
-
- $response = $sut->handle_request( $request );
- $this->assertSame( 500, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
-
- $debug = $data['errors'][0]['extensions']['debug'] ?? null;
- $this->assertIsArray( $debug );
- // The wrapper message is what surfaces here — and it's the message a
- // developer needs to identify the bug ("the resolver returned 0").
- $this->assertStringContainsString( 'out-of-range status code', $debug['message'] ?? '' );
- $this->assertStringContainsString( '0', $debug['message'] ?? '' );
-
- // Out-of-range path constructs the wrapper without a $previous, so
- // no previous chain should be attached.
- $this->assertArrayNotHasKey( 'previous', $data['errors'][0]['extensions'] );
- }
-
- /**
- * @testdox A throwing resolver without _debug=1 produces the canonical body — no debug or previous keys leak.
- */
- public function test_throwing_resolver_without_debug_param_produces_canonical_body(): void {
- // Even an admin: without _debug=1 the resolver-failure response stays
- // purely generic so no resolver internals leak.
- $admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
- wp_set_current_user( $admin );
-
- $sut = $this->controller_with_resolver( new AlwaysThrowingResolver() );
- $response = $sut->handle_request( $this->post_request( array( 'query' => '{ greeting { result } }' ) ) );
-
- $this->assertSame( 500, $response->get_status() );
- $data = $response->get_data();
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
- $this->assertArrayNotHasKey( 'debug', $data['errors'][0]['extensions'] );
- $this->assertArrayNotHasKey( 'previous', $data['errors'][0]['extensions'] );
-
- // Belt and braces: the resolver's exception message must NOT appear
- // anywhere in the serialized response.
- $wire = wp_json_encode( $data );
- $this->assertIsString( $wire );
- $this->assertStringNotContainsString( AlwaysThrowingResolver::THROW_MESSAGE, $wire );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerTest.php
deleted file mode 100644
index a898efe40d3..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLControllerTest.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLController as AutogeneratedGraphQLController;
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use WC_REST_Unit_Test_Case;
-
-/**
- * Tests for the GraphQLControllerBase class — specifically the HTTP methods
- * registered on the /wc/graphql route based on the GET endpoint option.
- */
-class GraphQLControllerTest extends WC_REST_Unit_Test_Case {
- /**
- * The System Under Test.
- *
- * @var GraphQLControllerBase
- */
- private $sut;
-
- /**
- * Set up before each test.
- *
- * Skips on PHP < 8.1 because GraphQLControllerBase uses PHP 8.0+ syntax in its
- * source file (named arguments). In production the class is only loaded
- * after {@see Main::is_enabled()} gates on PHP 8.1+; these tests bypass
- * that gate by hitting the DI container directly, so we replicate it here.
- */
- public function setUp(): void {
- parent::setUp();
-
- if ( PHP_VERSION_ID < 80100 ) {
- $this->markTestSkipped( 'GraphQLControllerBase requires PHP 8.1+.' );
- }
-
- // GraphQLControllerBase is abstract; instantiate the autogenerated subclass
- // that wc_get_container() also uses via Main::handle_rest_api_init_for_core().
- $this->sut = wc_get_container()->get( AutogeneratedGraphQLController::class );
- }
-
- /**
- * Clean up GraphQL options between tests.
- */
- public function tearDown(): void {
- delete_option( Main::OPTION_GET_ENDPOINT_ENABLED );
- delete_option( Main::OPTION_ENDPOINT_URL );
- delete_option( Main::OPTION_MAX_QUERY_DEPTH );
- delete_option( Main::OPTION_MAX_QUERY_COMPLEXITY );
- parent::tearDown();
- }
-
- /**
- * @testdox register exposes POST only when the GET endpoint option is disabled.
- */
- public function test_register_exposes_post_only_when_get_disabled(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
-
- $this->sut->register();
-
- $handlers = rest_get_server()->get_routes()['/wc/graphql'];
- $this->assertCount( 1, $handlers, 'Exactly one handler should be registered for /wc/graphql.' );
- $methods = $handlers[0]['methods'];
- $this->assertTrue( $methods['POST'] ?? false );
- $this->assertFalse( $methods['GET'] ?? false );
- }
-
- /**
- * @testdox register exposes GET and POST when the GET endpoint option is enabled.
- */
- public function test_register_exposes_get_and_post_when_get_enabled(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'yes' );
-
- $this->sut->register();
-
- $handlers = rest_get_server()->get_routes()['/wc/graphql'];
- $this->assertCount( 1, $handlers, 'Exactly one handler should be registered for /wc/graphql.' );
- $methods = $handlers[0]['methods'];
- $this->assertTrue( $methods['GET'] ?? false );
- $this->assertTrue( $methods['POST'] ?? false );
- }
-
- /**
- * @testdox get_endpoint_url returns the default when the option is unset.
- */
- public function test_get_endpoint_url_returns_default_when_option_unset(): void {
- delete_option( Main::OPTION_ENDPOINT_URL );
- $this->assertSame( GraphQLControllerBase::DEFAULT_ENDPOINT_URL, GraphQLControllerBase::get_endpoint_url() );
- }
-
- /**
- * @testdox get_endpoint_url returns the stored option value when it is well-formed.
- */
- public function test_get_endpoint_url_returns_option_value_when_valid(): void {
- update_option( Main::OPTION_ENDPOINT_URL, 'wc/v4/graphql' );
- $this->assertSame( 'wc/v4/graphql', GraphQLControllerBase::get_endpoint_url() );
- }
-
- /**
- * @testdox get_endpoint_url strips surrounding slashes from the stored option.
- */
- public function test_get_endpoint_url_strips_surrounding_slashes(): void {
- update_option( Main::OPTION_ENDPOINT_URL, '/wc/v4/graphql/' );
- $this->assertSame( 'wc/v4/graphql', GraphQLControllerBase::get_endpoint_url() );
- }
-
- /**
- * @testdox get_endpoint_url falls back to the default when the option has fewer than two segments.
- * @dataProvider provider_invalid_endpoint_url_values
- *
- * @param string $value The invalid option value.
- */
- public function test_get_endpoint_url_falls_back_on_invalid( string $value ): void {
- update_option( Main::OPTION_ENDPOINT_URL, $value );
- $this->assertSame( GraphQLControllerBase::DEFAULT_ENDPOINT_URL, GraphQLControllerBase::get_endpoint_url() );
- }
-
- /**
- * @testdox register uses the configured endpoint URL when the option is set.
- */
- public function test_register_uses_configured_endpoint_url(): void {
- update_option( Main::OPTION_ENDPOINT_URL, 'wc/v4/graphql' );
-
- $this->sut->register();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayHasKey( '/wc/v4/graphql', $routes, 'The configured endpoint URL should be registered.' );
- $this->assertArrayNotHasKey( '/wc/graphql', $routes, 'The default endpoint URL should not be registered when a custom one is set.' );
- }
-
- /**
- * Invalid values that the getter should replace with the default.
- *
- * @return array<string, array{string}>
- */
- public function provider_invalid_endpoint_url_values(): array {
- return array(
- 'empty string' => array( '' ),
- 'slashes only' => array( '///' ),
- 'single segment' => array( 'graphql' ),
- 'empty middle segment' => array( 'wc//graphql' ),
- 'invalid character' => array( 'wc/graph*ql' ),
- 'space in segment' => array( 'wc/my graphql' ),
- );
- }
-
- /**
- * @testdox get_max_query_depth returns the default when the option is unset.
- */
- public function test_get_max_query_depth_returns_default_when_option_unset(): void {
- delete_option( Main::OPTION_MAX_QUERY_DEPTH );
- $this->assertSame(
- GraphQLControllerBase::DEFAULT_MAX_QUERY_DEPTH,
- GraphQLControllerBase::get_max_query_depth()
- );
- }
-
- /**
- * @testdox get_max_query_depth returns the option value when it is a positive integer.
- */
- public function test_get_max_query_depth_returns_option_value_when_positive(): void {
- update_option( Main::OPTION_MAX_QUERY_DEPTH, '7' );
- $this->assertSame( 7, GraphQLControllerBase::get_max_query_depth() );
- }
-
- /**
- * @testdox get_max_query_depth falls back to the default when the option is empty, zero, or negative.
- * @dataProvider provider_non_positive_option_values
- *
- * @param string $value The non-positive option value.
- */
- public function test_get_max_query_depth_falls_back_on_non_positive( string $value ): void {
- update_option( Main::OPTION_MAX_QUERY_DEPTH, $value );
- $this->assertSame(
- GraphQLControllerBase::DEFAULT_MAX_QUERY_DEPTH,
- GraphQLControllerBase::get_max_query_depth()
- );
- }
-
- /**
- * @testdox get_max_query_complexity returns the default when the option is unset.
- */
- public function test_get_max_query_complexity_returns_default_when_option_unset(): void {
- delete_option( Main::OPTION_MAX_QUERY_COMPLEXITY );
- $this->assertSame(
- GraphQLControllerBase::DEFAULT_MAX_QUERY_COMPLEXITY,
- GraphQLControllerBase::get_max_query_complexity()
- );
- }
-
- /**
- * @testdox get_max_query_complexity returns the option value when it is a positive integer.
- */
- public function test_get_max_query_complexity_returns_option_value_when_positive(): void {
- update_option( Main::OPTION_MAX_QUERY_COMPLEXITY, '500' );
- $this->assertSame( 500, GraphQLControllerBase::get_max_query_complexity() );
- }
-
- /**
- * @testdox get_max_query_complexity falls back to the default when the option is empty, zero, or negative.
- * @dataProvider provider_non_positive_option_values
- *
- * @param string $value The non-positive option value.
- */
- public function test_get_max_query_complexity_falls_back_on_non_positive( string $value ): void {
- update_option( Main::OPTION_MAX_QUERY_COMPLEXITY, $value );
- $this->assertSame(
- GraphQLControllerBase::DEFAULT_MAX_QUERY_COMPLEXITY,
- GraphQLControllerBase::get_max_query_complexity()
- );
- }
-
- /**
- * Non-positive values that the getters should replace with the default.
- *
- * @return array<string, array{string}>
- */
- public function provider_non_positive_option_values(): array {
- return array(
- 'empty string' => array( '' ),
- 'zero' => array( '0' ),
- 'negative' => array( '-5' ),
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLEndpointRegistrarTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLEndpointRegistrarTest.php
deleted file mode 100644
index 35dcde85878..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/GraphQLEndpointRegistrarTest.php
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLController as AutogeneratedGraphQLController;
-use Automattic\WooCommerce\Internal\Api\GraphQLEndpointRegistrar;
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use WC_REST_Unit_Test_Case;
-
-/**
- * Tests for {@see GraphQLEndpointRegistrar} — the deferred-registration helper
- * used by sibling plugins via {@see Main::register_graphql_endpoint()}.
- */
-class GraphQLEndpointRegistrarTest extends WC_REST_Unit_Test_Case {
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->enable_or_disable_feature( true );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- $this->enable_or_disable_feature( false );
- delete_option( Main::OPTION_GET_ENDPOINT_ENABLED );
- parent::tearDown();
- }
-
- /**
- * Toggle the dual_code_graphql_api feature flag via its underlying option.
- *
- * @param bool $enable True to enable, false to disable.
- */
- private function enable_or_disable_feature( bool $enable ): void {
- update_option(
- wc_get_container()->get( FeaturesController::class )->feature_enable_option_name( 'dual_code_graphql_api' ),
- $enable ? 'yes' : 'no'
- );
- }
-
- /**
- * @testdox handle_rest_api_init registers a route with the configured methods.
- */
- public function test_handle_rest_api_init_registers_route_with_methods(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'yes' );
-
- $registrar = new GraphQLEndpointRegistrar(
- AutogeneratedGraphQLController::class,
- 'wc-test',
- '/registrar-route',
- array( 'GET', 'POST' )
- );
-
- $registrar->handle_rest_api_init();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayHasKey( '/wc-test/registrar-route', $routes );
- $methods = $routes['/wc-test/registrar-route'][0]['methods'];
- $this->assertTrue( $methods['GET'] ?? false );
- $this->assertTrue( $methods['POST'] ?? false );
- }
-
- /**
- * @testdox handle_rest_api_init applies the GET-endpoint setting.
- */
- public function test_handle_rest_api_init_strips_get_when_setting_is_off(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
-
- $registrar = new GraphQLEndpointRegistrar(
- AutogeneratedGraphQLController::class,
- 'wc-test',
- '/post-only-route',
- array( 'GET', 'POST' )
- );
-
- $registrar->handle_rest_api_init();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayHasKey( '/wc-test/post-only-route', $routes );
- $methods = $routes['/wc-test/post-only-route'][0]['methods'];
- $this->assertTrue( $methods['POST'] ?? false );
- $this->assertFalse( $methods['GET'] ?? false );
- }
-
- /**
- * @testdox handle_rest_api_init skips registration when settings reduce the methods to none.
- */
- public function test_handle_rest_api_init_skips_registration_when_no_methods_remain(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
-
- $registrar = new GraphQLEndpointRegistrar(
- AutogeneratedGraphQLController::class,
- 'wc-test',
- '/get-only-route',
- array( 'GET' )
- );
-
- $registrar->handle_rest_api_init();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayNotHasKey( '/wc-test/get-only-route', $routes );
- }
-
- /**
- * @testdox handle_rest_api_init skips registration when the feature is disabled.
- */
- public function test_handle_rest_api_init_skips_registration_when_feature_is_off(): void {
- $this->enable_or_disable_feature( false );
-
- $registrar = new GraphQLEndpointRegistrar(
- AutogeneratedGraphQLController::class,
- 'wc-test',
- '/disabled-feature-route',
- array( 'POST' )
- );
-
- $registrar->handle_rest_api_init();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayNotHasKey( '/wc-test/disabled-feature-route', $routes );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/HelperExceptionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/HelperExceptionsTest.php
deleted file mode 100644
index f97f2d691a1..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/HelperExceptionsTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\ApiException;
-use Automattic\WooCommerce\Api\ForbiddenException;
-use Automattic\WooCommerce\Api\InvalidTokenException;
-use Automattic\WooCommerce\Api\NotFoundException;
-use Automattic\WooCommerce\Api\UnauthorizedException;
-use Automattic\WooCommerce\Api\ValidationException;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the helper ApiException subclasses, each pinning a specific
- * (error code, HTTP status) pair so callers don't have to spell them out at
- * the throw site.
- *
- * The actual code → status mapping that turns these into HTTP responses lives
- * in {@see \Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase} and is
- * exercised end-to-end via {@see SecurityTest::test_invalid_token_error_code_maps_to_401()}
- * and similar; this file just verifies the exception classes themselves carry
- * the right metadata.
- */
-class HelperExceptionsTest extends WC_Unit_Test_Case {
- /**
- * @return array<string, array{class-string, string, int}>
- */
- public function provider_helper_exceptions(): array {
- return array(
- 'unauthorized' => array( UnauthorizedException::class, 'UNAUTHORIZED', 401 ),
- 'invalid_token' => array( InvalidTokenException::class, 'INVALID_TOKEN', 401 ),
- 'forbidden' => array( ForbiddenException::class, 'FORBIDDEN', 403 ),
- 'not_found' => array( NotFoundException::class, 'NOT_FOUND', 404 ),
- 'validation' => array( ValidationException::class, 'VALIDATION_ERROR', 422 ),
- );
- }
-
- /**
- * @testdox each helper extends ApiException and pins the expected code and HTTP status.
- *
- * @dataProvider provider_helper_exceptions
- *
- * @param class-string $class The helper exception class.
- * @param string $code The expected error code.
- * @param int $status_code The expected HTTP status code.
- */
- public function test_helper_exception_carries_code_and_status( string $class, string $code, int $status_code ): void {
- $exception = new $class();
-
- $this->assertInstanceOf( ApiException::class, $exception );
- $this->assertSame( $code, $exception->getErrorCode() );
- $this->assertSame( $status_code, $exception->getStatusCode() );
- $this->assertNotEmpty( $exception->getMessage() );
- $this->assertSame( array(), $exception->getExtensions() );
- }
-
- /**
- * @testdox each helper accepts a custom message, extensions, and previous throwable.
- *
- * @dataProvider provider_helper_exceptions
- *
- * @param class-string $class The helper exception class.
- */
- public function test_helper_exception_accepts_custom_args( string $class ): void {
- $previous = new \RuntimeException( 'inner' );
-
- $exception = new $class( 'Custom message.', array( 'detail' => 'extra' ), $previous );
-
- $this->assertSame( 'Custom message.', $exception->getMessage() );
- $this->assertSame( array( 'detail' => 'extra' ), $exception->getExtensions() );
- $this->assertSame( $previous, $exception->getPrevious() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/MainTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/MainTest.php
deleted file mode 100644
index 2eeab68f1b8..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/MainTest.php
+++ /dev/null
@@ -1,384 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLController as AutogeneratedGraphQLController;
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see Main} — the entry point that gates registration on PHP/feature
- * flag, exposes the GET-endpoint setting, and validates plugin-supplied
- * controller arguments.
- */
-class MainTest extends WC_Unit_Test_Case {
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->enable_or_disable_feature( true );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- $this->enable_or_disable_feature( false );
- delete_option( Main::OPTION_GET_ENDPOINT_ENABLED );
- parent::tearDown();
- }
-
- /**
- * Toggle the dual_code_graphql_api feature flag via its underlying option.
- *
- * @param bool $enable True to enable, false to disable.
- */
- private function enable_or_disable_feature( bool $enable ): void {
- update_option(
- wc_get_container()->get( FeaturesController::class )->feature_enable_option_name( 'dual_code_graphql_api' ),
- $enable ? 'yes' : 'no'
- );
- }
-
- /**
- * @testdox is_get_endpoint_enabled defaults to true when the option is absent.
- */
- public function test_is_get_endpoint_enabled_defaults_to_true(): void {
- delete_option( Main::OPTION_GET_ENDPOINT_ENABLED );
- $this->assertTrue( Main::is_get_endpoint_enabled() );
- }
-
- /**
- * @testdox is_get_endpoint_enabled honours the stored option value.
- */
- public function test_is_get_endpoint_enabled_reads_the_option(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
- $this->assertFalse( Main::is_get_endpoint_enabled() );
-
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'yes' );
- $this->assertTrue( Main::is_get_endpoint_enabled() );
- }
-
- /**
- * @testdox filter_methods_against_settings strips GET when the option is disabled.
- */
- public function test_filter_methods_strips_get_when_disabled(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
- $filtered = Main::filter_methods_against_settings( array( 'GET', 'POST' ) );
- $this->assertSame( array( 'POST' ), $filtered );
- }
-
- /**
- * @testdox filter_methods_against_settings keeps GET when the option is enabled.
- */
- public function test_filter_methods_keeps_get_when_enabled(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'yes' );
- $filtered = Main::filter_methods_against_settings( array( 'GET', 'POST' ) );
- $this->assertSame( array( 'GET', 'POST' ), $filtered );
- }
-
- /**
- * @testdox filter_methods_against_settings can leave the list empty when GET is the only entry and the option is off.
- */
- public function test_filter_methods_can_return_empty_array(): void {
- update_option( Main::OPTION_GET_ENDPOINT_ENABLED, 'no' );
- $this->assertSame( array(), Main::filter_methods_against_settings( array( 'GET' ) ) );
- }
-
- /**
- * @testdox is_enabled returns true when the feature flag is on.
- */
- public function test_is_enabled_when_feature_on(): void {
- $this->enable_or_disable_feature( true );
- $this->assertTrue( Main::is_enabled() );
- }
-
- /**
- * @testdox is_enabled returns false when the feature flag is off.
- */
- public function test_is_enabled_when_feature_off(): void {
- $this->enable_or_disable_feature( false );
- $this->assertFalse( Main::is_enabled() );
- }
-
- /**
- * @testdox instantiate_graphql_controller returns null when the feature is disabled.
- */
- public function test_instantiate_returns_null_when_disabled(): void {
- $this->enable_or_disable_feature( false );
- $this->assertNull( Main::instantiate_graphql_controller( AutogeneratedGraphQLController::class ) );
- }
-
- /**
- * @testdox instantiate_graphql_controller throws when the class is not a controller subclass.
- */
- public function test_instantiate_rejects_unrelated_classes(): void {
- $this->expectException( \InvalidArgumentException::class );
- Main::instantiate_graphql_controller( \stdClass::class );
- }
-
- /**
- * @testdox instantiate_graphql_controller throws when the class does not exist.
- */
- public function test_instantiate_rejects_missing_classes(): void {
- $this->expectException( \InvalidArgumentException::class );
- Main::instantiate_graphql_controller( 'Definitely\\Not\\A\\Class' );
- }
-
- /**
- * @testdox instantiate_graphql_controller returns a wired-up controller subclass when enabled.
- */
- public function test_instantiate_returns_controller_when_enabled(): void {
- $controller = Main::instantiate_graphql_controller( AutogeneratedGraphQLController::class );
-
- $this->assertInstanceOf( GraphQLControllerBase::class, $controller );
- }
-
- /**
- * @testdox register_graphql_endpoint is a silent no-op when the feature is disabled.
- */
- public function test_register_graphql_endpoint_is_a_no_op_when_disabled(): void {
- $this->enable_or_disable_feature( false );
-
- $routes_before = rest_get_server()->get_routes();
-
- // Must not throw and must leave the REST route map unchanged.
- Main::register_graphql_endpoint( AutogeneratedGraphQLController::class, 'wc-test', '/no-op' );
-
- $this->assertSame( $routes_before, rest_get_server()->get_routes() );
- }
-
- /**
- * @testdox register_graphql_endpoint rejects classes that are not GraphQLControllerBase subclasses.
- */
- public function test_register_graphql_endpoint_rejects_unrelated_classes(): void {
- $this->expectException( \InvalidArgumentException::class );
- Main::register_graphql_endpoint( \stdClass::class, 'wc-test', '/bogus' );
- }
-
- /**
- * @testdox register_graphql_endpoint rejects directories that don't contain a generated controller.
- */
- public function test_register_graphql_endpoint_rejects_directory_without_controller(): void {
- $tmp = sys_get_temp_dir() . '/wc-graphql-no-controller-' . uniqid();
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
- mkdir( $tmp, 0755, true );
-
- try {
- $this->expectException( \InvalidArgumentException::class );
- Main::register_graphql_endpoint( $tmp, 'wc-test', '/missing' );
- } finally {
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $tmp );
- }
- }
-
- /**
- * @testdox register hooks handle_rest_api_init_for_core onto rest_api_init.
- */
- public function test_register_hooks_rest_api_init(): void {
- remove_action( 'rest_api_init', array( Main::class, 'handle_rest_api_init_for_core' ) );
-
- Main::register();
-
- $this->assertNotFalse(
- has_action( 'rest_api_init', array( Main::class, 'handle_rest_api_init_for_core' ) ),
- 'register() should hook handle_rest_api_init_for_core onto rest_api_init.'
- );
-
- // Clean up so the hook doesn't leak into other tests.
- remove_action( 'rest_api_init', array( Main::class, 'handle_rest_api_init_for_core' ) );
- }
-
- /**
- * @testdox register also bootstraps Settings — its filter hooks become active.
- */
- public function test_register_bootstraps_settings(): void {
- // Snapshot the existing hook state so unrelated callbacks attached
- // elsewhere in the test process aren't dropped by remove_all_filters().
- $saved_sections = $GLOBALS['wp_filter']['woocommerce_get_sections_advanced'] ?? null;
- $saved_settings = $GLOBALS['wp_filter']['woocommerce_get_settings_advanced'] ?? null;
-
- remove_all_filters( 'woocommerce_get_sections_advanced' );
- remove_all_filters( 'woocommerce_get_settings_advanced' );
-
- try {
- Main::register();
-
- $this->assertNotFalse( has_filter( 'woocommerce_get_sections_advanced' ) );
- $this->assertNotFalse( has_filter( 'woocommerce_get_settings_advanced' ) );
- } finally {
- // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- restoring the snapshot taken above.
- if ( null === $saved_sections ) {
- unset( $GLOBALS['wp_filter']['woocommerce_get_sections_advanced'] );
- } else {
- $GLOBALS['wp_filter']['woocommerce_get_sections_advanced'] = $saved_sections;
- }
- if ( null === $saved_settings ) {
- unset( $GLOBALS['wp_filter']['woocommerce_get_settings_advanced'] );
- } else {
- $GLOBALS['wp_filter']['woocommerce_get_settings_advanced'] = $saved_settings;
- }
- // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
- remove_action( 'rest_api_init', array( Main::class, 'handle_rest_api_init_for_core' ) );
- }
- }
-
- /**
- * @testdox handle_rest_api_init_for_core registers the /wc/graphql route when the feature is enabled.
- */
- public function test_handle_rest_api_init_for_core_registers_route_when_enabled(): void {
- // Calling handle_rest_api_init_for_core() directly invokes
- // register_rest_route() outside of the rest_api_init action, which
- // triggers a _doing_it_wrong notice. Acknowledge it so the test base
- // class does not flag it as unexpected.
- $this->setExpectedIncorrectUsage( 'register_rest_route' );
-
- // Snapshot the original endpoints so we can restore them after the test,
- // regardless of what the SUT does to the shared WP_REST_Server.
- $server = rest_get_server();
- $reflection = new \ReflectionClass( $server );
- $prop = $reflection->getProperty( 'endpoints' );
- $prop->setAccessible( true );
- $original_endpoints = $prop->getValue( $server );
-
- try {
- // Force reset so we can observe registration deterministically.
- $endpoints = $original_endpoints;
- unset( $endpoints['/wc/graphql'] );
- $prop->setValue( $server, $endpoints );
-
- Main::handle_rest_api_init_for_core();
-
- $this->assertArrayHasKey( '/wc/graphql', rest_get_server()->get_routes() );
- } finally {
- $prop->setValue( $server, $original_endpoints );
- }
- }
-
- /**
- * @testdox handle_rest_api_init_for_core is a silent no-op when the feature is disabled.
- */
- public function test_handle_rest_api_init_for_core_is_noop_when_disabled(): void {
- $this->enable_or_disable_feature( false );
-
- // Snapshot the original endpoints so we can restore them after the test.
- // Without this, removing /wc/graphql here would leak into later tests.
- $server = rest_get_server();
- $reflection = new \ReflectionClass( $server );
- $prop = $reflection->getProperty( 'endpoints' );
- $prop->setAccessible( true );
- $original_endpoints = $prop->getValue( $server );
-
- try {
- $endpoints = $original_endpoints;
- unset( $endpoints['/wc/graphql'] );
- $prop->setValue( $server, $endpoints );
-
- Main::handle_rest_api_init_for_core();
-
- $this->assertArrayNotHasKey( '/wc/graphql', rest_get_server()->get_routes() );
- } finally {
- $prop->setValue( $server, $original_endpoints );
- }
- }
-
- /**
- * @testdox resolve_controller_class returns its argument unchanged when given an FQCN.
- */
- public function test_resolve_controller_class_passes_through_fqcn(): void {
- $reflection = new \ReflectionClass( Main::class );
- $method = $reflection->getMethod( 'resolve_controller_class' );
- $method->setAccessible( true );
-
- $result = $method->invoke( null, 'Some\\Plugin\\GraphQLController' );
-
- $this->assertSame( 'Some\\Plugin\\GraphQLController', $result );
- }
-
- /**
- * @testdox resolve_controller_class extracts the namespace from a generated controller file.
- */
- public function test_resolve_controller_class_extracts_namespace_from_directory(): void {
- $tmp_root = sys_get_temp_dir() . '/wc-graphql-resolve-' . uniqid();
- $dir = $tmp_root . '/src/Internal/Api/Autogenerated';
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
- mkdir( $dir, 0755, true );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents,WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents
- file_put_contents(
- $dir . '/GraphQLController.php',
- "<?php\nnamespace Some\\Plugin\\Internal\\Api\\Autogenerated;\nclass GraphQLController {}\n"
- );
-
- $reflection = new \ReflectionClass( Main::class );
- $method = $reflection->getMethod( 'resolve_controller_class' );
- $method->setAccessible( true );
-
- try {
- $result = $method->invoke( null, $tmp_root );
- $this->assertSame( 'Some\\Plugin\\Internal\\Api\\Autogenerated\\GraphQLController', $result );
- } finally {
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_unlink,WordPress.WP.AlternativeFunctions.unlink_unlink
- unlink( $dir . '/GraphQLController.php' );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $dir );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $tmp_root . '/src/Internal/Api' );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $tmp_root . '/src/Internal' );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $tmp_root . '/src' );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- rmdir( $tmp_root );
- }
- }
-
- /**
- * @testdox extract_namespace_from_php_source handles common PHP source forms.
- *
- * @dataProvider provider_php_source_forms
- *
- * @param string $source PHP source fragment.
- * @param ?string $expected The expected extracted namespace, or null when none.
- */
- public function test_extract_namespace_handles_various_source_forms( string $source, ?string $expected ): void {
- $reflection = new \ReflectionClass( Main::class );
- $method = $reflection->getMethod( 'extract_namespace_from_php_source' );
- $method->setAccessible( true );
-
- $this->assertSame( $expected, $method->invoke( null, $source ) );
- }
-
- /**
- * @return array<string, array{0: string, 1: ?string}>
- */
- public function provider_php_source_forms(): array {
- return array(
- 'standard multi-line' => array(
- "<?php\n\nnamespace Foo\\Bar\\Baz;\n\nclass Quux {}\n",
- 'Foo\\Bar\\Baz',
- ),
- 'single-line declaration' => array(
- '<?php namespace Foo\\Bar; class Quux {}',
- 'Foo\\Bar',
- ),
- 'declare(strict_types) before' => array(
- "<?php\ndeclare(strict_types=1);\nnamespace Foo\\Bar;\nclass Quux {}\n",
- 'Foo\\Bar',
- ),
- 'no namespace declared' => array(
- "<?php\nclass Quux {}\n",
- null,
- ),
- 'leading and trailing slashes' => array(
- "<?php\nnamespace \\Foo\\Bar\\;\nclass Quux {}\n",
- 'Foo\\Bar',
- ),
- );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/MetadataAttributeTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/MetadataAttributeTest.php
deleted file mode 100644
index 5f1af955ca3..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/MetadataAttributeTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Attributes\Internal;
-use Automattic\WooCommerce\Api\Attributes\Metadata;
-use WC_Unit_Test_Case;
-
-/**
- * Unit tests for the {@see Metadata} base attribute and the {@see Internal}
- * convenience subclass. ApiBuilder identifies metadata-bearing attributes via
- * `instanceof Metadata`, so the contract these tests pin is the
- * `get_name()` / `get_value()` pair on the base class.
- */
-class MetadataAttributeTest extends WC_Unit_Test_Case {
- /**
- * @return array<string, array{bool|int|float|string|null}>
- */
- public function provider_scalar_values(): array {
- return array(
- 'bool true' => array( true ),
- 'bool false' => array( false ),
- 'int' => array( 42 ),
- 'float' => array( 3.14 ),
- 'string' => array( 'core-team' ),
- 'null' => array( null ),
- );
- }
-
- /**
- * @testdox Metadata round-trips name and value for every supported scalar type.
- *
- * @dataProvider provider_scalar_values
- * @param bool|int|float|string|null $value Value to round-trip.
- */
- public function test_round_trip_for_scalar_values( bool|int|float|string|null $value ): void {
- $metadata = new Metadata( 'sample', $value );
-
- $this->assertSame( 'sample', $metadata->get_name() );
- $this->assertSame( $value, $metadata->get_value() );
- }
-
- /**
- * @testdox Internal subclass produces a Metadata entry named "internal" with value true.
- */
- public function test_internal_subclass_carries_internal_true(): void {
- $internal = new Internal();
-
- $this->assertInstanceOf( Metadata::class, $internal );
- $this->assertSame( 'internal', $internal->get_name() );
- $this->assertTrue( $internal->get_value() );
- }
-
- /**
- * @testdox Metadata is repeatable so multiple distinct names can decorate one element.
- */
- public function test_metadata_attribute_is_repeatable(): void {
- $reflection = new \ReflectionClass( Metadata::class );
- $attributes = $reflection->getAttributes( \Attribute::class );
-
- $this->assertNotEmpty( $attributes, 'Metadata should be decorated with #[Attribute].' );
-
- $attribute = $attributes[0]->newInstance();
- $this->assertNotSame( 0, $attribute->flags & \Attribute::IS_REPEATABLE );
- }
-
- /**
- * @testdox Metadata::shows_in_metadata_query() defaults to true so existing entries surface through `_apiMetadata`.
- */
- public function test_shows_in_metadata_query_defaults_to_true(): void {
- $metadata = new Metadata( 'sample', 'value' );
- $this->assertTrue( $metadata->shows_in_metadata_query() );
- }
-
- /**
- * @testdox A Metadata subclass can override shows_in_metadata_query() to opt out of `_apiMetadata` exposure.
- */
- public function test_shows_in_metadata_query_can_be_overridden_to_false(): void {
- $hidden = new class('hidden', 'value') extends Metadata {
- /**
- * Opt the carrying target out of the `_apiMetadata` query.
- */
- public function shows_in_metadata_query(): bool {
- return false;
- }
- };
-
- $this->assertFalse( $hidden->shows_in_metadata_query() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/OpcacheFileExpiryTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/OpcacheFileExpiryTest.php
deleted file mode 100644
index 797dcf66d76..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/OpcacheFileExpiryTest.php
+++ /dev/null
@@ -1,125 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\OpcacheFileExpiry;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see OpcacheFileExpiry} — TTL-based deletion of cached files.
- */
-class OpcacheFileExpiryTest extends WC_Unit_Test_Case {
-
- /**
- * Track temp dirs for removal in tearDown.
- *
- * @var string[]
- */
- private array $temp_dirs_to_clean = array();
-
- /**
- * Skip on PHP < 8.1 because OpcacheFileExpiry imports from the GraphQL
- * stack autoloaded only on PHP 8.1+.
- */
- public function setUp(): void {
- parent::setUp();
- if ( PHP_VERSION_ID < 80100 ) {
- $this->markTestSkipped( 'OpcacheFileExpiry tests require PHP 8.1+.' );
- }
- }
-
- /**
- * Clean up filters, temp dirs, and scheduled actions between tests.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_graphql_opcache_cache_dir' );
- foreach ( $this->temp_dirs_to_clean as $dir ) {
- $this->rrmdir( $dir );
- }
- $this->temp_dirs_to_clean = array();
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( OpcacheFileExpiry::ACTION_HOOK );
- }
- parent::tearDown();
- }
-
- /**
- * @testdox delete_expired_files removes only files whose mtime is older than the TTL.
- */
- public function test_delete_expired_files_removes_only_expired(): void {
- $dir = $this->register_temp_cache_dir();
-
- $fresh = $dir . '/' . str_repeat( 'a', 64 ) . '.php';
- $expired = $dir . '/' . str_repeat( 'b', 64 ) . '.php';
- file_put_contents( $fresh, '<?php return array();' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
- file_put_contents( $expired, '<?php return array();' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
- touch( $expired, time() - QueryCache::get_cache_ttl() - 1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch
-
- $deleted = OpcacheFileExpiry::delete_expired_files();
-
- $this->assertSame( 1, $deleted );
- $this->assertFileExists( $fresh );
- $this->assertFileDoesNotExist( $expired );
- }
-
- /**
- * @testdox delete_expired_files returns 0 when the cache directory does not exist.
- */
- public function test_delete_expired_files_returns_zero_when_dir_missing(): void {
- add_filter(
- 'woocommerce_graphql_opcache_cache_dir',
- static function () {
- return '/nonexistent/path/that/does/not/exist';
- }
- );
-
- $this->assertSame( 0, OpcacheFileExpiry::delete_expired_files() );
- }
-
- /**
- * Create a per-test cache directory, point the OPcache filter at it, and
- * register it for cleanup in tearDown.
- */
- private function register_temp_cache_dir(): string {
- $dir = sys_get_temp_dir() . '/wc-graphql-cleanup-test-' . bin2hex( random_bytes( 6 ) );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
- mkdir( $dir, 0700, true );
-
- add_filter(
- 'woocommerce_graphql_opcache_cache_dir',
- static function () use ( $dir ) {
- return $dir;
- }
- );
-
- $this->temp_dirs_to_clean[] = $dir;
- return $dir;
- }
-
- /**
- * Recursively remove a directory tree.
- *
- * @param string $dir Path to remove.
- */
- private function rrmdir( string $dir ): void {
- if ( ! is_dir( $dir ) ) {
- return;
- }
- // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- foreach ( scandir( $dir ) as $entry ) {
- if ( '.' === $entry || '..' === $entry ) {
- continue;
- }
- $path = $dir . '/' . $entry;
- if ( is_dir( $path ) ) {
- $this->rrmdir( $path );
- } else {
- wp_delete_file( $path );
- }
- }
- rmdir( $dir );
- // phpcs:enable
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php
deleted file mode 100644
index 7b3cbeb266a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php
+++ /dev/null
@@ -1,495 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Internal\Api\OpcacheFileExpiry;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see QueryCache} — covers the AST cache backing both the
- * standard "parse + cache" path and the Apollo Automatic Persisted Queries
- * (APQ) protocol, as well as the OPTION_OBJECT_CACHE_ENABLED toggle.
- */
-class QueryCacheTest extends WC_Unit_Test_Case {
- /**
- * The System Under Test.
- *
- * @var QueryCache
- */
- private QueryCache $sut;
-
- /**
- * Set up before each test.
- *
- * Skips on PHP < 8.1 because the GraphQL stack (vendor parser, QueryCache
- * dependencies) is only autoloaded after {@see Main::is_enabled()} gates
- * on PHP 8.1+. Replicate that gate here so the autoload never triggers a
- * parse error on older PHP.
- */
- public function setUp(): void {
- parent::setUp();
-
- if ( PHP_VERSION_ID < 80100 ) {
- $this->markTestSkipped( 'QueryCache tests require PHP 8.1+.' );
- }
-
- // OPcache caching defaults to 'yes'; turn it off so the existing
- // object-cache assertions aren't bypassed by a writable filesystem.
- // Individual tests may opt back in.
- update_option( Main::OPTION_OPCACHE_ENABLED, 'no' );
-
- wp_cache_flush();
- $this->sut = new QueryCache();
- }
-
- /**
- * Clean up the option and cache between tests.
- */
- public function tearDown(): void {
- delete_option( Main::OPTION_OBJECT_CACHE_ENABLED );
- delete_option( Main::OPTION_OPCACHE_ENABLED );
- remove_all_filters( 'woocommerce_graphql_opcache_cache_dir' );
- foreach ( $this->temp_dirs_to_clean as $dir ) {
- $this->rrmdir( $dir );
- }
- $this->temp_dirs_to_clean = array();
- foreach ( $this->temp_files_to_clean as $file ) {
- if ( file_exists( $file ) ) {
- wp_delete_file( $file );
- }
- }
- $this->temp_files_to_clean = array();
- if ( function_exists( 'as_unschedule_all_actions' ) ) {
- as_unschedule_all_actions( OpcacheFileExpiry::ACTION_HOOK );
- }
- wp_cache_flush();
- parent::tearDown();
- }
-
- /**
- * @testdox resolve parses a plain query and returns a DocumentNode.
- */
- public function test_resolve_parses_a_plain_query(): void {
- $result = $this->sut->resolve( '{ widget { id } }', array() );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- }
-
- /**
- * @testdox resolve returns the cached AST on the second call for the same query.
- */
- public function test_resolve_returns_cached_document_on_second_call(): void {
- $first = $this->sut->resolve( '{ widget { id } }', array() );
- $second = $this->sut->resolve( '{ widget { id } }', array() );
-
- $this->assertInstanceOf( DocumentNode::class, $first );
- $this->assertInstanceOf( DocumentNode::class, $second );
- // Distinct instances are fine; both must represent the same parsed query.
- $this->assertEquals( $first->toArray(), $second->toArray() );
- }
-
- /**
- * @testdox resolve returns a BAD_REQUEST error when called with a null query and no APQ.
- */
- public function test_resolve_rejects_null_query_without_apq(): void {
- $result = $this->sut->resolve( null, array() );
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'errors', $result );
- $this->assertSame( 'No query provided.', $result['errors'][0]['message'] ?? null );
- $this->assertSame( 'BAD_REQUEST', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox resolve surfaces a syntax error as GRAPHQL_PARSE_ERROR.
- */
- public function test_resolve_returns_parse_error_for_invalid_syntax(): void {
- $result = $this->sut->resolve( '{ widget { id', array() );
-
- $this->assertIsArray( $result );
- $this->assertSame( 'GRAPHQL_PARSE_ERROR', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox apq registers a query when both the query and matching hash are provided.
- */
- public function test_apq_registers_when_query_and_matching_hash_are_provided(): void {
- $query = '{ widget { id } }';
- $hash = hash( 'sha256', $query );
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => $hash,
- ),
- );
-
- $first = $this->sut->resolve( $query, $extensions );
- $this->assertInstanceOf( DocumentNode::class, $first );
-
- // Subsequent hash-only request must hit the cache.
- $second = $this->sut->resolve( null, $extensions );
- $this->assertInstanceOf( DocumentNode::class, $second );
- }
-
- /**
- * @testdox apq returns PERSISTED_QUERY_HASH_MISMATCH when the supplied hash doesn't match the query.
- */
- public function test_apq_rejects_query_when_hash_does_not_match(): void {
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => str_repeat( 'a', 64 ),
- ),
- );
-
- $result = $this->sut->resolve( '{ widget { id } }', $extensions );
-
- $this->assertIsArray( $result );
- $this->assertSame( 'PERSISTED_QUERY_HASH_MISMATCH', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox apq returns PERSISTED_QUERY_NOT_FOUND when the hash is unknown.
- */
- public function test_apq_returns_not_found_when_hash_is_unknown(): void {
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => str_repeat( 'b', 64 ),
- ),
- );
-
- $result = $this->sut->resolve( null, $extensions );
-
- $this->assertIsArray( $result );
- $this->assertSame( 'PERSISTED_QUERY_NOT_FOUND', $result['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox apq is ignored when the version is not 1 — falls through to the standard path.
- */
- public function test_apq_falls_through_when_version_is_not_one(): void {
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 2,
- 'sha256Hash' => str_repeat( 'c', 64 ),
- ),
- );
-
- $result = $this->sut->resolve( '{ widget { id } }', $extensions );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- }
-
- /**
- * @testdox apq is ignored when the hash is not 64-char lowercase hex — guards the OPcache include path against traversal.
- */
- public function test_apq_rejects_malformed_hash(): void {
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => 'not-a-sha256-hash',
- ),
- );
-
- $result = $this->sut->resolve( '{ widget { id } }', $extensions );
-
- $this->assertInstanceOf(
- DocumentNode::class,
- $result,
- 'A malformed APQ hash must bypass APQ dispatch so it never reaches the OPcache include path.'
- );
- }
-
- /**
- * @testdox get_cache_ttl exposes the configured TTL.
- */
- public function test_get_cache_ttl_is_a_day(): void {
- $this->assertSame( DAY_IN_SECONDS, QueryCache::get_cache_ttl() );
- }
-
- /**
- * @testdox resolve writes the parsed document to the object cache when the toggle is on.
- */
- public function test_resolve_writes_to_cache_when_toggle_on(): void {
- update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'yes' );
-
- $result = $this->sut->resolve( '{ __typename }', array() );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- $this->assertNotFalse(
- wp_cache_get( $this->cache_key_for( '{ __typename }' ), 'wc-graphql' ),
- 'Standard parse should persist the AST in the object cache.'
- );
- }
-
- /**
- * @testdox resolve does not write to the object cache when the toggle is off.
- */
- public function test_resolve_does_not_write_to_cache_when_toggle_off(): void {
- update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'no' );
-
- $result = $this->sut->resolve( '{ __typename }', array() );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- $this->assertFalse(
- wp_cache_get( $this->cache_key_for( '{ __typename }' ), 'wc-graphql' ),
- 'No cache entry should be written when the ObjectCache toggle is off.'
- );
- }
-
- /**
- * @testdox resolve treats a malformed object-cache entry as a cache miss and reparses.
- */
- public function test_resolve_treats_malformed_object_cache_entry_as_miss(): void {
- update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'yes' );
-
- $query = '{ __typename }';
- wp_cache_set( $this->cache_key_for( $query ), array( 'not' => 'a valid AST' ), 'wc-graphql' );
-
- $result = $this->sut->resolve( $query, array() );
-
- $this->assertInstanceOf(
- DocumentNode::class,
- $result,
- 'A corrupted cache payload must be treated as a miss and the query reparsed.'
- );
- }
-
- /**
- * @testdox resolve writes a parsed AST as a PHP file when OPcache is enabled and the dir is writable.
- */
- public function test_resolve_writes_to_opcache_file_when_toggle_on(): void {
- $dir = $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
-
- $query = '{ widget { id } }';
- $result = $this->sut->resolve( $query, array() );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- $this->assertFileExists( $dir . '/' . hash( 'sha256', $query ) . '.php' );
- }
-
- /**
- * @testdox resolve does not write to the OPcache dir when the toggle is off.
- */
- public function test_resolve_does_not_write_to_opcache_file_when_toggle_off(): void {
- $dir = $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'no' );
-
- $query = '{ widget { id } }';
- $this->sut->resolve( $query, array() );
-
- $this->assertFileDoesNotExist( $dir . '/' . hash( 'sha256', $query ) . '.php' );
- }
-
- /**
- * @testdox resolve returns the AST from the OPcache file on the second call.
- */
- public function test_resolve_returns_document_from_opcache_on_second_call(): void {
- $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
-
- $query = '{ widget { id } }';
- $first = $this->sut->resolve( $query, array() );
- $second = $this->sut->resolve( $query, array() );
-
- $this->assertInstanceOf( DocumentNode::class, $first );
- $this->assertInstanceOf( DocumentNode::class, $second );
- $this->assertEquals( $first->toArray(), $second->toArray() );
- }
-
- /**
- * @testdox apq registration persists across the OPcache file backend.
- */
- public function test_apq_round_trip_via_opcache(): void {
- $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
-
- $query = '{ widget { id } }';
- $hash = hash( 'sha256', $query );
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => $hash,
- ),
- );
-
- $register = $this->sut->resolve( $query, $extensions );
- $this->assertInstanceOf( DocumentNode::class, $register );
-
- $lookup = $this->sut->resolve( null, $extensions );
- $this->assertInstanceOf( DocumentNode::class, $lookup );
- }
-
- /**
- * @testdox apq hash-only lookup resolves from the object cache when OPcache becomes unavailable after registration.
- */
- public function test_apq_lookup_falls_back_to_object_cache_when_opcache_disabled(): void {
- $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
-
- $query = '{ widget { id } }';
- $hash = hash( 'sha256', $query );
- $extensions = array(
- 'persistedQuery' => array(
- 'version' => 1,
- 'sha256Hash' => $hash,
- ),
- );
-
- $register = $this->sut->resolve( $query, $extensions );
- $this->assertInstanceOf( DocumentNode::class, $register );
-
- update_option( Main::OPTION_OPCACHE_ENABLED, 'no' );
- $this->sut = new QueryCache();
-
- $lookup = $this->sut->resolve( null, $extensions );
- $this->assertInstanceOf(
- DocumentNode::class,
- $lookup,
- 'APQ hash-only lookup must still resolve from the object cache after OPcache is disabled.'
- );
- }
-
- /**
- * @testdox resolve falls back to the object cache when the OPcache dir is not writable.
- */
- public function test_resolve_falls_back_to_object_cache_when_opcache_dir_unwritable(): void {
- $this->skip_if_opcache_disabled();
-
- $not_a_dir = tempnam( sys_get_temp_dir(), 'wc-graphql-cache-' );
- $this->temp_files_to_clean[] = $not_a_dir;
-
- add_filter(
- 'woocommerce_graphql_opcache_cache_dir',
- static function () use ( $not_a_dir ) {
- return $not_a_dir;
- }
- );
-
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
- update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'yes' );
-
- $result = $this->sut->resolve( '{ __typename }', array() );
-
- $this->assertInstanceOf( DocumentNode::class, $result );
- $this->assertNotFalse(
- wp_cache_get( $this->cache_key_for( '{ __typename }' ), 'wc-graphql' ),
- 'Should have fallen back to the object cache when the OPcache dir is unwritable.'
- );
- }
-
- /**
- * @testdox writing to the OPcache backend schedules the cleanup sweep on first write.
- */
- public function test_first_write_schedules_cleanup(): void {
- $this->use_temp_opcache_dir();
- update_option( Main::OPTION_OPCACHE_ENABLED, 'yes' );
-
- $this->assertFalse(
- as_has_scheduled_action( OpcacheFileExpiry::ACTION_HOOK ),
- 'Pre-condition: no cleanup action should be scheduled before the first write.'
- );
-
- $this->sut->resolve( '{ __typename }', array() );
-
- $this->assertTrue(
- as_has_scheduled_action( OpcacheFileExpiry::ACTION_HOOK ),
- 'A successful OPcache write must schedule the cleanup sweep.'
- );
- }
-
- /**
- * Skip the calling test if OPcache is not enabled in this environment.
- *
- * Typical for PHP CLI without opcache.enable_cli=1 — the file-backend
- * capability check requires opcache_get_status to report enabled, so
- * tests that exercise that path are not meaningful without it.
- */
- private function skip_if_opcache_disabled(): void {
- if ( ! function_exists( 'opcache_get_status' ) || ! ini_get( 'opcache.enable' ) ) {
- $this->markTestSkipped( 'OPcache is not enabled in this environment.' );
- }
- $status = opcache_get_status( false );
- if ( ! is_array( $status ) || empty( $status['opcache_enabled'] ) ) {
- $this->markTestSkipped( 'OPcache is not enabled in this environment.' );
- }
- }
-
- /**
- * Point the OPcache backend at a per-test temp dir, return the path, and
- * register a teardown hook to remove it.
- */
- private function use_temp_opcache_dir(): string {
- $this->skip_if_opcache_disabled();
-
- $dir = sys_get_temp_dir() . '/wc-graphql-test-' . bin2hex( random_bytes( 6 ) );
- // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
- mkdir( $dir, 0700, true );
-
- add_filter(
- 'woocommerce_graphql_opcache_cache_dir',
- static function () use ( $dir ) {
- return $dir;
- }
- );
-
- $this->temp_dirs_to_clean[] = $dir;
-
- return $dir;
- }
-
- /**
- * Track temp dirs for removal in tearDown.
- *
- * @var string[]
- */
- private array $temp_dirs_to_clean = array();
-
- /**
- * Track temp files for removal in tearDown.
- *
- * @var string[]
- */
- private array $temp_files_to_clean = array();
-
- /**
- * Recursively remove a directory tree.
- *
- * @param string $dir Path to remove.
- */
- private function rrmdir( string $dir ): void {
- if ( ! is_dir( $dir ) ) {
- return;
- }
- // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
- foreach ( scandir( $dir ) as $entry ) {
- if ( '.' === $entry || '..' === $entry ) {
- continue;
- }
- $path = $dir . '/' . $entry;
- if ( is_dir( $path ) ) {
- $this->rrmdir( $path );
- } else {
- wp_delete_file( $path );
- }
- }
- rmdir( $dir );
- // phpcs:enable
- }
-
- /**
- * Build the QueryCache cache key for a query string. Prefix kept in sync
- * with QueryCache::CACHE_KEY_PREFIX.
- *
- * @param string $query The GraphQL query string.
- */
- private function cache_key_for( string $query ): string {
- return 'graphql_ast_v15_' . hash( 'sha256', $query );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/QueryComplexityRuleTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/QueryComplexityRuleTest.php
deleted file mode 100644
index eaf5772e8bf..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/QueryComplexityRuleTest.php
+++ /dev/null
@@ -1,289 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\QueryComplexityRule;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see QueryComplexityRule}.
- *
- * The rule is exercised through DocumentValidator against a small hand-built
- * schema, the same way GraphQLControllerBase wires it (stock rules plus ours).
- */
-class QueryComplexityRuleTest extends WC_Unit_Test_Case {
- /**
- * Number of times the Counted scalar's parseValue callback ran.
- *
- * @var int
- */
- private int $parse_value_calls = 0;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->parse_value_calls = 0;
- }
-
- /**
- * Build the test schema.
- *
- * type Query { a: String, b: String, c: String, item: Item, items(first: Int): [Item], counted(values: [Counted!]): String }
- * type Item { id: Int, name: String }
- * scalar Counted (parseValue is instrumented)
- *
- * `items` carries a complexity callback multiplying the children's score
- * by `first`, like WooCommerce's connection fields do.
- */
- private function build_schema(): Schema {
- $counted = new CustomScalarType(
- array(
- 'name' => 'Counted',
- 'serialize' => static fn( $value ) => $value,
- 'parseValue' => function ( $value ) {
- ++$this->parse_value_calls;
- return $value;
- },
- 'parseLiteral' => static fn( $node ) => $node->value ?? null,
- )
- );
-
- $item = new ObjectType(
- array(
- 'name' => 'Item',
- 'fields' => array(
- 'id' => Type::int(),
- 'name' => Type::string(),
- ),
- )
- );
-
- $query = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => array(
- 'a' => Type::string(),
- 'b' => Type::string(),
- 'c' => Type::string(),
- 'item' => $item,
- 'items' => array(
- 'type' => Type::listOf( $item ),
- 'args' => array( 'first' => Type::int() ),
- 'complexity' => static fn( int $children, array $args ): int => ( $args['first'] ?? 1 ) * ( $children + 1 ),
- ),
- 'counted' => array(
- 'type' => Type::string(),
- 'args' => array( 'values' => Type::listOf( Type::nonNull( $counted ) ) ),
- ),
- ),
- )
- );
-
- return new Schema( array( 'query' => $query ) );
- }
-
- /**
- * Validate a document with the stock rules plus a QueryComplexityRule.
- *
- * @param string $query The GraphQL document.
- * @param int $max_complexity The complexity limit.
- * @param array $variables Raw variable values, as sent by the client.
- * @param bool $only_this_rule When true, validate with the complexity rule alone (no stock rules).
- * @param ?QueryComplexityRule $rule A pre-built (e.g. instrumented) rule instance to use instead of a fresh one.
- * @return array{0: Error[], 1: QueryComplexityRule} The validation errors and the rule instance.
- */
- private function validate( string $query, int $max_complexity, array $variables = array(), bool $only_this_rule = false, ?QueryComplexityRule $rule = null ): array {
- $sut = $rule ?? new QueryComplexityRule( $max_complexity );
- $sut->setRawVariableValues( $variables );
-
- $rules = $only_this_rule ? array() : array_values( DocumentValidator::allRules() );
- $rules[] = $sut;
-
- $errors = DocumentValidator::validate( $this->build_schema(), Parser::parse( $query ), $rules );
-
- return array( $errors, $sut );
- }
-
- /**
- * Build a document in which each named fragment spreads the next one twice,
- * so the number of spreads reachable from the root doubles with every fragment.
- *
- * @param int $fragment_count Number of chained fragments.
- * @param string $leaf Selection set body of the last fragment.
- */
- private function build_duplicate_spread_chain( int $fragment_count, string $leaf = 'a' ): string {
- $document = "query Q { ...F0 }\n";
- for ( $i = 0; $i < $fragment_count - 1; $i++ ) {
- $next = $i + 1;
- $document .= "fragment F{$i} on Query { ...F{$next} ...F{$next} }\n";
- }
- $last = $fragment_count - 1;
-
- return $document . "fragment F{$last} on Query { {$leaf} }\n";
- }
-
- /**
- * @testdox Duplicate fragment spreads are scored once per fragment, so a document whose fragments spread each other twice is scored in linear work.
- */
- public function test_duplicate_fragment_spreads_are_scored_once_per_fragment(): void {
- $fragment_count = 40;
- $query = $this->build_duplicate_spread_chain( $fragment_count );
-
- $sut = new class( 1000 ) extends QueryComplexityRule {
- /**
- * Number of selection sets scored.
- *
- * @var int
- */
- public int $selection_sets_scored = 0;
-
- /**
- * Count the call, then score as usual.
- *
- * @param SelectionSetNode $selection_set The selection set to score.
- */
- protected function fieldComplexity( SelectionSetNode $selection_set ): int {
- ++$this->selection_sets_scored;
- return parent::fieldComplexity( $selection_set );
- }
- };
-
- list( $errors ) = $this->validate( $query, 1000, array(), false, $sut );
-
- // The operation's selection set plus each fragment's exactly once, rather than once per spread.
- $this->assertSame( $fragment_count + 1, $sut->selection_sets_scored );
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query complexity exceeded.', $errors[0]->getMessage() );
- // Memoization must not change the score: the leaf still counts once per spread.
- $this->assertSame( 2 ** 39, $sut->getQueryComplexity() );
- }
-
- /**
- * @testdox Fragment spreads are scored exactly as if the fragment had been written inline.
- */
- public function test_fragment_spread_scores_match_inline_expansion(): void {
- $with_fragments = '{ ...F0 } fragment F0 on Query { ...F1 ...F1 } fragment F1 on Query { a b }';
- $inline = '{ a b a b }';
-
- list( $errors, $sut ) = $this->validate( $with_fragments, 4 );
- $this->assertSame( array(), $errors );
- $this->assertSame( 4, $sut->getQueryComplexity() );
-
- list( , $inline_sut ) = $this->validate( $inline, 4 );
- $this->assertSame( $inline_sut->getQueryComplexity(), $sut->getQueryComplexity() );
-
- list( $errors ) = $this->validate( $with_fragments, 3 );
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query complexity exceeded.', $errors[0]->getMessage() );
- }
-
- /**
- * @testdox The computed score saturates at COMPLEXITY_CEILING instead of overflowing PHP's int.
- */
- public function test_score_saturates_instead_of_overflowing(): void {
- // 2^69 would overflow a 64-bit int (and surface as a TypeError).
- $query = $this->build_duplicate_spread_chain( 70 );
-
- list( $errors, $sut ) = $this->validate( $query, 1000 );
-
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query complexity exceeded.', $errors[0]->getMessage() );
- $this->assertSame( QueryComplexityRule::COMPLEXITY_CEILING, $sut->getQueryComplexity() );
- }
-
- /**
- * @testdox A fragment cycle terminates and is scored as exceeding the limit, even without the NoFragmentCycles rule.
- */
- public function test_fragment_cycle_terminates(): void {
- $query = '{ ...A } fragment A on Query { a ...B } fragment B on Query { b ...A }';
-
- list( $errors ) = $this->validate( $query, 1000, array(), true );
-
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query complexity exceeded.', $errors[0]->getMessage() );
- }
-
- /**
- * @testdox Variable values are coerced once per document, not once per @include/@skip directive.
- */
- public function test_variables_are_coerced_once_per_document(): void {
- $query = 'query Q($values: [Counted!], $flag: Boolean!) {
- a @include(if: $flag)
- b @include(if: $flag)
- c @skip(if: $flag)
- counted(values: $values)
- }';
-
- list( $errors, $sut ) = $this->validate(
- $query,
- 1000,
- array(
- 'values' => array( 1, 2, 3 ),
- 'flag' => true,
- )
- );
-
- $this->assertSame( array(), $errors );
- // a, b, counted (c is skipped).
- $this->assertSame( 3, $sut->getQueryComplexity() );
- // One parseValue call per list element, regardless of how many directives the document carries.
- $this->assertSame( 3, $this->parse_value_calls );
- }
-
- /**
- * @testdox Fields excluded by @include / @skip (literal or variable-driven) don't count, including when both directives are present.
- */
- public function test_include_and_skip_directives_exclude_fields(): void {
- list( $errors, $sut ) = $this->validate( '{ a @include(if: false) b @skip(if: true) c @include(if: true) @skip(if: true) item { id } }', 1000 );
- $this->assertSame( array(), $errors );
- $this->assertSame( 2, $sut->getQueryComplexity() );
-
- list( $errors, $sut ) = $this->validate(
- 'query Q($show: Boolean!) { a @include(if: $show) b @skip(if: $show) }',
- 1000,
- array( 'show' => false )
- );
- $this->assertSame( array(), $errors );
- $this->assertSame( 1, $sut->getQueryComplexity() );
- }
-
- /**
- * @testdox Complexity callbacks receive the coerced field arguments, for fields both in operations and inside fragments.
- */
- public function test_complexity_callback_receives_arguments(): void {
- list( $errors, $sut ) = $this->validate( 'query Q($n: Int) { items(first: $n) { id name } }', 1000, array( 'n' => 10 ) );
- $this->assertSame( array(), $errors );
- // 10 * (2 children + 1).
- $this->assertSame( 30, $sut->getQueryComplexity() );
-
- list( $errors, $sut ) = $this->validate( '{ ...F } fragment F on Query { items(first: 5) { id } }', 1000 );
- $this->assertSame( array(), $errors );
- $this->assertSame( 10, $sut->getQueryComplexity() );
-
- list( $errors ) = $this->validate( '{ items(first: 100) { id name } }', 100 );
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query complexity exceeded.', $errors[0]->getMessage() );
- }
-
- /**
- * @testdox Missing required variables surface as a coercion error rather than as a crash.
- */
- public function test_missing_required_variable_is_reported(): void {
- $this->expectException( Error::class );
- $this->expectExceptionMessageMatches( '/\$flag/' );
-
- $this->validate( 'query Q($flag: Boolean!) { a @include(if: $flag) }', 1000, array() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/QueryDepthRuleTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/QueryDepthRuleTest.php
deleted file mode 100644
index ebe27d9374c..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/QueryDepthRuleTest.php
+++ /dev/null
@@ -1,176 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Internal\Api\QueryDepthRule;
-use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
-use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
-use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
-use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for {@see QueryDepthRule}.
- *
- * Depth is counted the way the stock webonyx rule counts it: the nesting
- * level (root = 0) of the deepest field that itself has a selection set.
- * `{ node { leaf } }` has depth 0, `{ node { node { leaf } } }` has depth 1.
- */
-class QueryDepthRuleTest extends WC_Unit_Test_Case {
- /**
- * Build the test schema: type Query { node: Node, leaf: String } type Node { node: Node, leaf: String }.
- */
- private function build_schema(): Schema {
- $node = new ObjectType(
- array(
- 'name' => 'Node',
- 'fields' => static function () use ( &$node ): array {
- return array(
- 'node' => $node,
- 'leaf' => Type::string(),
- );
- },
- )
- );
-
- $query = new ObjectType(
- array(
- 'name' => 'Query',
- 'fields' => array(
- 'node' => $node,
- 'leaf' => Type::string(),
- ),
- )
- );
-
- return new Schema( array( 'query' => $query ) );
- }
-
- /**
- * Validate a document with the stock rules plus a QueryDepthRule.
- *
- * @param string $query The GraphQL document.
- * @param int $max_depth The depth limit.
- * @param bool $only_this_rule When true, validate with the depth rule alone (no stock rules).
- * @param ?QueryDepthRule $rule A pre-built (e.g. instrumented) rule instance to use instead of a fresh one.
- * @return Error[] The validation errors.
- */
- private function validate( string $query, int $max_depth, bool $only_this_rule = false, ?QueryDepthRule $rule = null ): array {
- $sut = $rule ?? new QueryDepthRule( $max_depth );
-
- $rules = $only_this_rule ? array() : array_values( DocumentValidator::allRules() );
- $rules[] = $sut;
-
- return DocumentValidator::validate( $this->build_schema(), Parser::parse( $query ), $rules );
- }
-
- /**
- * Assert that a document is exactly at the given depth: accepted with that
- * limit, rejected with one less.
- *
- * @param int $expected_depth The expected depth (must be >= 2, since a limit of 0 disables the rule).
- * @param string $query The GraphQL document.
- */
- private function assert_depth( int $expected_depth, string $query ): void {
- $this->assertSame( array(), $this->validate( $query, $expected_depth ), "Expected depth {$expected_depth} to be accepted." );
-
- $errors = $this->validate( $query, $expected_depth - 1 );
- $this->assertCount( 1, $errors, 'Expected depth ' . ( $expected_depth - 1 ) . ' to be rejected.' );
- $this->assertSame( 'Maximum query depth exceeded.', $errors[0]->getMessage() );
- }
-
- /**
- * @testdox Duplicate fragment spreads are walked once per fragment, so a document whose fragments spread each other twice is validated in linear work.
- */
- public function test_duplicate_fragment_spreads_are_walked_once_per_fragment(): void {
- // Each fragment spreads the next one twice.
- $fragment_count = 40;
- $query = "query Q { ...F0 }\n";
- for ( $i = 0; $i < $fragment_count - 1; $i++ ) {
- $next = $i + 1;
- $query .= "fragment F{$i} on Query { ...F{$next} ...F{$next} }\n";
- }
- $query .= 'fragment F' . ( $fragment_count - 1 ) . " on Query { leaf }\n";
-
- $sut = new class( 15 ) extends QueryDepthRule {
- /**
- * Number of selection trees walked.
- *
- * @var int
- */
- public int $trees_walked = 0;
-
- /**
- * Count the call, then walk as usual.
- *
- * @param Node $node The node whose selection set is walked.
- * @param int $depth The depth the node sits at.
- * @param int $max_depth The maximum depth seen so far.
- */
- protected function fieldDepth( Node $node, int $depth = 0, int $max_depth = 0 ): int {
- ++$this->trees_walked;
- return parent::fieldDepth( $node, $depth, $max_depth );
- }
- };
-
- $errors = $this->validate( $query, 15, false, $sut );
-
- // The operation plus each fragment exactly once, rather than once per spread.
- $this->assertSame( $fragment_count + 1, $sut->trees_walked );
- $this->assertSame( array(), $errors );
- }
-
- /**
- * @testdox A fragment's depth is counted relative to the position it is spread at.
- */
- public function test_fragment_depth_is_relative_to_spread_position(): void {
- $fragment = ' fragment F on Node { node { leaf } }';
-
- $this->assert_depth( 2, '{ node { node { ...F } } }' . $fragment );
- $this->assert_depth( 3, '{ node { node { node { ...F } } } }' . $fragment );
- }
-
- /**
- * @testdox The same fragment spread at two different depths counts at the deeper one.
- */
- public function test_same_fragment_at_different_depths_counts_the_deepest(): void {
- $query = '{ shallow: node { ...F } deep: node { node { ...F } } } fragment F on Node { node { leaf } }';
-
- $this->assert_depth( 2, $query );
- }
-
- /**
- * @testdox A fragment with no nested selections adds no depth wherever it is spread.
- */
- public function test_fragment_without_nested_fields_adds_no_depth(): void {
- $query = '{ node { node { node { ...F } } } } fragment F on Node { leaf }';
-
- $this->assert_depth( 2, $query );
- }
-
- /**
- * @testdox Nested fragment spreads compose their relative depths.
- */
- public function test_nested_fragment_spreads_compose(): void {
- $query = '{ node { ...F } } fragment F on Node { node { ...G } } fragment G on Node { node { node { leaf } } }';
-
- $this->assert_depth( 3, $query );
- }
-
- /**
- * @testdox A fragment cycle terminates and is reported as exceeding the limit, even without the NoFragmentCycles rule.
- */
- public function test_fragment_cycle_terminates(): void {
- $query = '{ ...A } fragment A on Query { node { ...B } } fragment B on Query { node { ...A } }';
-
- $errors = $this->validate( $query, 100, true );
-
- $this->assertCount( 1, $errors );
- $this->assertSame( 'Maximum query depth exceeded.', $errors[0]->getMessage() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/README.md b/plugins/woocommerce/tests/php/src/Internal/Api/README.md
deleted file mode 100644
index eebdefbb45d..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# GraphQL infrastructure tests
-
-Unit tests for the manually-maintained code under `src/Internal/Api/` and the resolver tree generated by `ApiBuilder` against a dummy code-API kept under `Fixtures/DummyApi/`.
-
-The command tree under `src/Api/Queries/` and `src/Api/Mutations/` (products, coupons, …) is covered by the same `wc-phpunit-graphql` testsuite; those tests live under `tests/php/src/Api/Queries/` and `tests/php/src/Api/Mutations/`.
-
-## These tests require PHP 8.1+
-
-The dummy fixture API uses PHP 8.1+ syntax (enums, named arguments, `readonly` promoted properties, etc.). PHPUnit cannot even parse those files on PHP 7.4 / 8.0, so the `wc-phpunit-graphql` suite must never be discovered on those versions.
-
-The default testsuite list in `phpunit.xml` (`defaultTestSuite`) includes `wc-phpunit-graphql`, so a plain `phpunit` / `pnpm test:php` run on **PHP 8.1+** picks these tests up automatically.
-
-On **PHP 7.4 / 8.0** you must restrict the run to the two version-agnostic suites, or PHPUnit will parse-fatal at discovery:
-
-```sh
-pnpm test:php:env -- --testsuite=wc-phpunit-legacy,wc-phpunit-main
-```
-
-This is exactly what the PHP 7.4 CI jobs do.
-
-## Running the tests locally (PHP 8.1+ only)
-
-Run the whole set (the default) or just the GraphQL suite:
-
-```sh
-# The full set (default — legacy + main + GraphQL):
-pnpm test:php:env
-
-# Just the GraphQL tests (infrastructure + public API commands):
-pnpm test:php:env -- --testsuite=wc-phpunit-graphql
-```
-
-Without `pnpm`:
-
-```sh
-./vendor/bin/phpunit -c phpunit.xml
-./vendor/bin/phpunit -c phpunit.xml --testsuite=wc-phpunit-graphql
-```
-
-## PHP < 8.1 fallback tests
-
-The few tests that verify the GraphQL feature **gracefully degrades** on PHP versions where it's unavailable (`Settings::add_section()` / `add_settings()` returning the input unchanged) live outside this directory, in `tests/php/src/Internal/LegacyPhpApi/`. They have to live there because they need to be discovered by the `wc-phpunit-main` suite, which runs on every PHP version (including the PHP 7.4 / 8.0 jobs) — anything under `Internal/Api/` is excluded from `wc-phpunit-main` by string-prefix match.
-
-## Regenerating the dummy code-API
-
-Whenever the code-API fixture under `Fixtures/DummyApi/` changes, regenerate the matching tree under `Fixtures/DummyApiAutogenerated/` and commit both:
-
-```sh
-pnpm build:api:test
-```
-
-The script invokes `build-api.php` with the fixture paths and namespaces. It does not run `composer dump-autoload` because both fixture namespaces are already covered by the existing `Automattic\WooCommerce\Tests\` PSR-4 prefix.
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/SecurityTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/SecurityTest.php
deleted file mode 100644
index 621a6ea3fe1..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/SecurityTest.php
+++ /dev/null
@@ -1,211 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Infrastructure\ClassResolver as DummyContainer;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApi\Store as DummyStore;
-use Automattic\WooCommerce\Tests\Internal\Api\Fixtures\DummyApiAutogenerated\GraphQLController as DummyGraphQLController;
-use WC_REST_Unit_Test_Case;
-
-/**
- * Security-focused tests for {@see GraphQLControllerBase} and the resolvers it
- * dispatches to.
- *
- * Anonymous and admin paths are covered elsewhere; this file pins down the
- * authenticated-but-low-privilege scenarios (editor / subscriber), the
- * aliased-field cap-enforcement contract, the `authorize()`
- * exception-translation paths, and the route-level authorization marker.
- */
-class SecurityTest extends WC_REST_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var GraphQLControllerBase
- */
- private GraphQLControllerBase $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- wp_cache_flush();
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
-
- $this->sut = new DummyGraphQLController();
- $this->sut->init( new QueryCache() );
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- DummyStore::reset();
- DummyContainer::reset();
- wp_set_current_user( 0 );
- wp_cache_flush();
- parent::tearDown();
- }
-
- /**
- * Build a POST request to `/wc/graphql` with the given body params.
- *
- * @param array $body Request body params (query, variables, …).
- */
- private function post_request( array $body ): \WP_REST_Request {
- $request = new \WP_REST_Request( 'POST', '/wc/graphql' );
- foreach ( $body as $key => $value ) {
- $request->set_param( $key, $value );
- }
- return $request;
- }
-
- /**
- * @testdox an editor user (lacks manage_options) is rejected on a #[RequiredCapability]-gated query.
- */
- public function test_editor_user_is_rejected_on_capability_protected_query(): void {
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ widget(id: 1) { id } }' ) )
- );
-
- $this->assertSame( 403, $response->get_status() );
- $this->assertSame( 'FORBIDDEN', $response->get_data()['errors'][0]['extensions']['code'] ?? null );
- }
-
- /**
- * @testdox a subscriber user (lacks manage_woocommerce) is blocked from introspection.
- */
- public function test_subscriber_user_is_blocked_from_introspection(): void {
- $subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
- wp_set_current_user( $subscriber );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ __schema { queryType { name } } }' ) )
- );
-
- $data = $response->get_data();
- // The DisableIntrospection rule produces a validation error; the
- // formatter promotes ClientAware errors with no explicit code to
- // BAD_USER_INPUT (HTTP 400).
- $this->assertSame( 400, $response->get_status() );
- $this->assertNotEmpty( $data['errors'] ?? array() );
- }
-
- /**
- * @testdox aliased calls to a capability-protected field enforce the cap on each alias.
- */
- public function test_aliased_capability_protected_fields_enforce_cap_per_alias(): void {
- $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
- wp_set_current_user( $editor );
-
- $response = $this->sut->handle_request(
- $this->post_request(
- array( 'query' => '{ a: widget(id: 1) { id } b: widget(id: 2) { id } }' )
- )
- );
-
- $this->assertSame( 403, $response->get_status() );
-
- $data = $response->get_data();
- $codes = array_map(
- static fn( array $err ): ?string => $err['extensions']['code'] ?? null,
- $data['errors'] ?? array()
- );
- $forbidden_count = count(
- array_filter( $codes, static fn( ?string $code ): bool => 'FORBIDDEN' === $code )
- );
- $this->assertGreaterThanOrEqual(
- 2,
- $forbidden_count,
- 'Both aliased resolutions must enforce the cap independently.'
- );
- }
-
- /**
- * @testdox an authorize() that throws ApiException carries its custom code through to the wire.
- */
- public function test_authorize_throwing_api_exception_carries_code_through(): void {
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ authorizeThrows(kind: "api_exception") { result } }' ) )
- );
-
- $data = $response->get_data();
- $this->assertSame( 'AUTH_FAILURE', $data['errors'][0]['extensions']['code'] ?? null );
- $this->assertSame( 'Authorize failed.', $data['errors'][0]['message'] ?? null );
- $this->assertSame( 'extra', $data['errors'][0]['extensions']['detail'] ?? null );
- }
-
- /**
- * @testdox an INVALID_TOKEN error code maps to HTTP 401.
- *
- * Regression test: INVALID_TOKEN is the canonical code for plugin-supplied
- * principal resolvers signalling "credentials present but wrong" (vs.
- * UNAUTHORIZED for "credentials missing"). Without an entry in the
- * status map, it would default to HTTP 500 — which is what callers saw
- * before this entry was added.
- */
- public function test_invalid_token_error_code_maps_to_401(): void {
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ authorizeThrows(kind: "invalid_token") { result } }' ) )
- );
-
- $this->assertSame( 401, $response->get_status() );
- $this->assertSame(
- 'INVALID_TOKEN',
- $response->get_data()['errors'][0]['extensions']['code'] ?? null
- );
- }
-
- /**
- * @testdox an authorize() that throws an arbitrary exception is masked behind INTERNAL_ERROR.
- */
- public function test_authorize_throwing_runtime_exception_masks_message(): void {
- wp_set_current_user( 0 );
-
- $response = $this->sut->handle_request(
- $this->post_request( array( 'query' => '{ authorizeThrows(kind: "runtime") { result } }' ) )
- );
-
- $data = $response->get_data();
- $this->assertSame( 500, $response->get_status() );
- $this->assertSame( 'INTERNAL_ERROR', $data['errors'][0]['extensions']['code'] ?? null );
- // The wrapping GraphQLError carries 'An unexpected error occurred.', but
- // because its `previous` is a non-ClientAware throwable webonyx replaces
- // the message with its generic 'Internal server error' on the wire. The
- // security-relevant invariant is that the original exception message
- // never leaves the resolver.
- $this->assertStringNotContainsString(
- 'Internals leaked from authorize.',
- wp_json_encode( $data['errors'][0] )
- );
- }
-
- /**
- * @testdox the REST route registers with `__return_true` as its permission_callback.
- *
- * Auth is enforced per-query / per-mutation in the generated resolvers, not
- * at the REST route level. Any future change that adds a route-level
- * permission gate (e.g., `manage_woocommerce`) would silently break the
- * public queries — this test pins the design intent.
- */
- public function test_rest_route_uses_return_true_permission_callback(): void {
- $this->sut->register();
-
- $routes = rest_get_server()->get_routes();
- $this->assertArrayHasKey( '/wc/graphql', $routes );
- $this->assertSame( '__return_true', $routes['/wc/graphql'][0]['permission_callback'] );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php
deleted file mode 100644
index f8e6569a996..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php
+++ /dev/null
@@ -1,316 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\Api;
-
-use Automattic\WooCommerce\Api\Infrastructure\GraphQLControllerBase;
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Internal\Api\QueryCache;
-use Automattic\WooCommerce\Internal\Api\Settings;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the GraphQL API Settings class.
- */
-class SettingsTest extends WC_Unit_Test_Case {
- /**
- * The System Under Test.
- *
- * @var Settings
- */
- private $sut;
-
- /**
- * Set up before each test.
- */
- public function setUp(): void {
- parent::setUp();
- $this->enable_or_disable_feature( true );
- $this->sut = new Settings();
- }
-
- /**
- * Clean up filters and options registered by tests so global state doesn't leak.
- */
- public function tearDown(): void {
- remove_filter( 'woocommerce_get_sections_advanced', array( $this->sut, 'add_section' ) );
- remove_filter( 'woocommerce_get_settings_advanced', array( $this->sut, 'add_settings' ), 10 );
- remove_filter(
- 'woocommerce_admin_settings_sanitize_option_' . Main::OPTION_ENDPOINT_URL,
- array( $this->sut, 'sanitize_endpoint_url' ),
- 10
- );
- delete_option( Main::OPTION_ENDPOINT_URL );
- $this->enable_or_disable_feature( false );
- parent::tearDown();
- }
-
- /**
- * Enable or disable the GraphQL API feature.
- *
- * @param bool $enable True to enable, false to disable.
- */
- private function enable_or_disable_feature( bool $enable ): void {
- update_option(
- wc_get_container()->get( FeaturesController::class )->feature_enable_option_name( 'dual_code_graphql_api' ),
- $enable ? 'yes' : 'no'
- );
- }
-
- /**
- * @testdox register hooks add_section and add_settings into WooCommerce's advanced settings filters.
- */
- public function test_register_hooks_both_advanced_filters(): void {
- $this->sut->register();
-
- $this->assertNotFalse(
- has_filter( 'woocommerce_get_sections_advanced', array( $this->sut, 'add_section' ) ),
- 'add_section should be hooked to woocommerce_get_sections_advanced.'
- );
- $this->assertNotFalse(
- has_filter( 'woocommerce_get_settings_advanced', array( $this->sut, 'add_settings' ) ),
- 'add_settings should be hooked to woocommerce_get_settings_advanced.'
- );
- }
-
- /**
- * @testdox add_section appends the graphql section while preserving existing ones.
- */
- public function test_add_section_appends_graphql_section(): void {
- $result = $this->sut->add_section( array( 'features' => 'Features' ) );
-
- $this->assertArrayHasKey( Settings::SECTION_ID, $result );
- $this->assertArrayHasKey( 'features', $result );
- }
-
- /**
- * @testdox add_settings defines the GET endpoint checkbox with a 'yes' default.
- */
- public function test_add_settings_defines_get_endpoint_checkbox(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_GET_ENDPOINT_ENABLED, $by_id );
- $this->assertSame( 'checkbox', $by_id[ Main::OPTION_GET_ENDPOINT_ENABLED ]['type'] );
- $this->assertSame( 'yes', $by_id[ Main::OPTION_GET_ENDPOINT_ENABLED ]['default'] );
- }
-
- /**
- * @testdox add_settings defines the APQ checkbox with a 'yes' default (PHP 8.1+).
- */
- public function test_add_settings_defines_apq_checkbox(): void {
- if ( PHP_VERSION_ID < 80100 ) {
- $this->markTestSkipped( 'GraphQL settings require PHP 8.1+.' );
- }
-
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_APQ_ENABLED, $by_id );
- $this->assertSame( 'checkbox', $by_id[ Main::OPTION_APQ_ENABLED ]['type'] );
- $this->assertSame( 'yes', $by_id[ Main::OPTION_APQ_ENABLED ]['default'] );
- }
-
- /**
- * @testdox add_settings defines the endpoint URL text field with the default constant as default.
- */
- public function test_add_settings_defines_endpoint_url_field(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_ENDPOINT_URL, $by_id );
- $this->assertSame( 'text', $by_id[ Main::OPTION_ENDPOINT_URL ]['type'] );
- $this->assertSame( GraphQLControllerBase::DEFAULT_ENDPOINT_URL, $by_id[ Main::OPTION_ENDPOINT_URL ]['default'] );
- }
-
- /**
- * @testdox sanitize_endpoint_url returns the normalized input for a well-formed URL.
- */
- public function test_sanitize_endpoint_url_accepts_valid_url(): void {
- $result = $this->sut->sanitize_endpoint_url( null, array(), 'wc/v4/graphql' );
- $this->assertSame( 'wc/v4/graphql', $result );
- }
-
- /**
- * @testdox sanitize_endpoint_url strips surrounding slashes.
- */
- public function test_sanitize_endpoint_url_strips_surrounding_slashes(): void {
- $result = $this->sut->sanitize_endpoint_url( null, array(), '/wc/v4/graphql/' );
- $this->assertSame( 'wc/v4/graphql', $result );
- }
-
- /**
- * @testdox sanitize_endpoint_url rejects invalid input and returns the previously stored value.
- * @dataProvider provider_invalid_endpoint_url_inputs
- *
- * @param string $raw_input The raw submitted value.
- */
- public function test_sanitize_endpoint_url_rejects_invalid_input( string $raw_input ): void {
- update_option( Main::OPTION_ENDPOINT_URL, 'wc/v4/graphql' );
-
- $result = $this->sut->sanitize_endpoint_url( null, array(), $raw_input );
-
- $this->assertSame( 'wc/v4/graphql', $result, 'Invalid input should not overwrite the previously stored value.' );
- }
-
- /**
- * Inputs the sanitize handler should reject.
- *
- * @return array<string, array{string}>
- */
- public function provider_invalid_endpoint_url_inputs(): array {
- return array(
- 'empty string' => array( '' ),
- 'slashes only' => array( '///' ),
- 'single segment' => array( 'graphql' ),
- 'spaces in segment' => array( 'wc/my graphql' ),
- 'special characters' => array( 'wc/graph*ql' ),
- );
- }
-
- /**
- * @testdox sanitize_endpoint_url falls back to the stored value when the raw input is not a string.
- * @dataProvider provider_non_string_endpoint_url_inputs
- *
- * @param mixed $raw_input The raw submitted value (null, array, etc.).
- */
- public function test_sanitize_endpoint_url_handles_non_string_input( $raw_input ): void {
- update_option( Main::OPTION_ENDPOINT_URL, 'wc/v4/graphql' );
-
- $result = $this->sut->sanitize_endpoint_url( null, array(), $raw_input );
-
- $this->assertSame( 'wc/v4/graphql', $result, 'Non-string input should not overwrite the previously stored value.' );
- }
-
- /**
- * Non-string raw inputs the sanitize handler may receive from POST data.
- *
- * @return array<string, array{mixed}>
- */
- public function provider_non_string_endpoint_url_inputs(): array {
- return array(
- 'null' => array( null ),
- 'array' => array( array( 'wc/graphql' ) ),
- );
- }
-
- /**
- * @testdox add_settings returns the input unchanged on PHP < 8.1.
- */
- public function test_add_settings_is_noop_on_unsupported_php(): void {
- if ( PHP_VERSION_ID >= 80100 ) {
- $this->markTestSkipped( 'Only relevant on PHP < 8.1.' );
- }
-
- $input = array( array( 'id' => 'existing' ) );
- $result = $this->sut->add_settings( $input, Settings::SECTION_ID );
-
- $this->assertSame( $input, $result );
- }
-
- /**
- * @testdox add_settings defines the ObjectCache checkbox with a 'yes' default.
- */
- public function test_add_settings_defines_object_cache_checkbox(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_OBJECT_CACHE_ENABLED, $by_id );
- $this->assertSame( 'checkbox', $by_id[ Main::OPTION_OBJECT_CACHE_ENABLED ]['type'] );
- $this->assertSame( 'yes', $by_id[ Main::OPTION_OBJECT_CACHE_ENABLED ]['default'] );
- }
-
- /**
- * @testdox add_settings defines the OPcache checkbox with a 'yes' default.
- */
- public function test_add_settings_defines_opcache_checkbox(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_OPCACHE_ENABLED, $by_id );
- $this->assertSame( 'checkbox', $by_id[ Main::OPTION_OPCACHE_ENABLED ]['type'] );
- $this->assertSame( 'yes', $by_id[ Main::OPTION_OPCACHE_ENABLED ]['default'] );
- }
-
- /**
- * @testdox add_settings defines the max query depth field with min=1 and the default constant as default.
- */
- public function test_add_settings_defines_max_query_depth_field(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_MAX_QUERY_DEPTH, $by_id );
- $this->assertSame( 'number', $by_id[ Main::OPTION_MAX_QUERY_DEPTH ]['type'] );
- $this->assertSame(
- (string) GraphQLControllerBase::DEFAULT_MAX_QUERY_DEPTH,
- $by_id[ Main::OPTION_MAX_QUERY_DEPTH ]['default']
- );
- $this->assertSame( '1', $by_id[ Main::OPTION_MAX_QUERY_DEPTH ]['custom_attributes']['min'] );
- }
-
- /**
- * @testdox add_settings defines the max query complexity field with min=1 and the default constant as default.
- */
- public function test_add_settings_defines_max_query_complexity_field(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_MAX_QUERY_COMPLEXITY, $by_id );
- $this->assertSame( 'number', $by_id[ Main::OPTION_MAX_QUERY_COMPLEXITY ]['type'] );
- $this->assertSame(
- (string) GraphQLControllerBase::DEFAULT_MAX_QUERY_COMPLEXITY,
- $by_id[ Main::OPTION_MAX_QUERY_COMPLEXITY ]['default']
- );
- $this->assertSame( '1', $by_id[ Main::OPTION_MAX_QUERY_COMPLEXITY ]['custom_attributes']['min'] );
- }
-
- /**
- * @testdox add_settings returns the original settings unchanged when the section id does not match.
- */
- public function test_add_settings_passes_through_for_other_sections(): void {
- $existing = array( array( 'id' => 'placeholder' ) );
-
- $result = $this->sut->add_settings( $existing, 'some_other_section' );
-
- $this->assertSame( $existing, $result );
- }
-
- /**
- * @testdox add_section returns sections unchanged when the feature is disabled.
- */
- public function test_add_section_does_not_register_when_feature_is_off(): void {
- $this->enable_or_disable_feature( false );
-
- $result = $this->sut->add_section( array( 'features' => 'Features' ) );
-
- $this->assertArrayNotHasKey( Settings::SECTION_ID, $result );
- }
-
- /**
- * @testdox add_settings returns settings unchanged when the feature is disabled.
- */
- public function test_add_settings_does_not_register_when_feature_is_off(): void {
- $this->enable_or_disable_feature( false );
-
- $result = $this->sut->add_settings( array(), Settings::SECTION_ID );
-
- $this->assertSame( array(), $result );
- }
-
- /**
- * @testdox add_settings defines the parsed query cache TTL field with min=1 and the default constant as default.
- */
- public function test_add_settings_defines_query_cache_ttl_field(): void {
- $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
- $by_id = array_column( $fields, null, 'id' );
-
- $this->assertArrayHasKey( Main::OPTION_QUERY_CACHE_TTL, $by_id );
- $this->assertSame( 'number', $by_id[ Main::OPTION_QUERY_CACHE_TTL ]['type'] );
- $this->assertSame(
- (string) QueryCache::DEFAULT_CACHE_TTL,
- $by_id[ Main::OPTION_QUERY_CACHE_TTL ]['default']
- );
- $this->assertSame( '1', $by_id[ Main::OPTION_QUERY_CACHE_TTL ]['custom_attributes']['min'] );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/LegacyPhpApi/SettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/LegacyPhpApi/SettingsTest.php
deleted file mode 100644
index 8eda4bced40..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/LegacyPhpApi/SettingsTest.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\LegacyPhpApi;
-
-use Automattic\WooCommerce\Api\Infrastructure\Main;
-use Automattic\WooCommerce\Internal\Api\Settings;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the GraphQL API Settings class on PHP versions where the
- * dual-code GraphQL feature is unavailable (PHP < 8.1).
- *
- * Lives outside `tests/php/src/Internal/Api/` because that directory is
- * excluded from the default testsuite (its dummy fixture API uses PHP 8.1+
- * syntax). These tests must run on PHP 7.4 / 8.0 to verify that
- * {@see Settings::add_section()} and {@see Settings::add_settings()}
- * gracefully degrade to a no-op when {@see Main::is_enabled()} returns false.
- *
- * `Settings.php` and `Main.php` are intentionally PHP 7.4-parseable, and the
- * {@see Main::is_enabled()} short-circuit prevents the methods from ever
- * reaching the lines that reference PHP 8.1+ classes such as `GraphQLController`.
- */
-class SettingsTest extends WC_Unit_Test_Case {
- /**
- * The system under test.
- *
- * @var Settings
- */
- private $sut;
-
- /**
- * Set up.
- */
- public function setUp(): void {
- parent::setUp();
- $this->enable_or_disable_feature( true );
- $this->sut = new Settings();
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- $this->enable_or_disable_feature( false );
- parent::tearDown();
- }
-
- /**
- * Toggle the dual_code_graphql_api feature flag via its underlying option.
- *
- * @param bool $enable True to enable, false to disable.
- */
- private function enable_or_disable_feature( bool $enable ): void {
- update_option(
- wc_get_container()->get( FeaturesController::class )->feature_enable_option_name( 'dual_code_graphql_api' ),
- $enable ? 'yes' : 'no'
- );
- }
-
- /**
- * @testdox add_section is a no-op on PHP < 8.1.
- */
- public function test_add_section_is_noop_on_unsupported_php(): void {
- if ( PHP_VERSION_ID >= 80100 ) {
- $this->markTestSkipped( 'Only relevant on PHP < 8.1.' );
- }
-
- $input = array( 'features' => 'Features' );
- $result = $this->sut->add_section( $input );
-
- $this->assertSame( $input, $result );
- }
-
- /**
- * @testdox add_settings returns the input unchanged on PHP < 8.1.
- */
- public function test_add_settings_is_noop_on_unsupported_php(): void {
- if ( PHP_VERSION_ID >= 80100 ) {
- $this->markTestSkipped( 'Only relevant on PHP < 8.1.' );
- }
-
- $input = array( array( 'id' => 'existing' ) );
- $result = $this->sut->add_settings( $input, Settings::SECTION_ID );
-
- $this->assertSame( $input, $result );
- }
-}