Read from the package's own PHP files at the commit 7a76c621b953: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.
ApiclassOne shop's APIs, for the gateway's actions.
1 method · 3 properties
DocumentsclassThe GraphQL documents the actions send.
10 constants
ProductsclassShopify's two product shapes, read as one Product.
4 methods
ShopifyGatewayFactoryclassShopify, a commerce platform: a sale paid on the shop's invoice page (a draft order), the shop's orders, its catalogue (products, variants, inventory) and its webhooks.
16 options
FetchInventoryActionclassThe variants' inventory: inventoryQuantity (summed over the locations) when inventoryItem.tracked, else a null quantity. The variants by their gids (or bare ids) through nodes(ids:), 250 at a time; none given, every variant of the shop through productVariants, page by page.
3 methods
FetchOrderActionclassA Shopify order, by its gid or its bare id.
4 methods
FetchProductActionclassOne product: by its gid, its bare numeric id, its handle, or the address of a page of it - the storefront's /products/<handle> (the handle is the last segment) or the admin's /admin/products/<id>. Null when the shop has no such product.
4 methods
FetchProductsActionclassA page of the shop's products, by the Admin GraphQL products(...) query, the least recently updated first: the cursor is Shopify's endCursor, updatedSince becomes the search filter updated_at:>'...' and the query is appended to it as Shopify's own search text. Pass the same updatedSince and query with the cursor: Shopify's cursor is only a position.
4 methods
FetchTransactionActionclassWhat became of a draft order: completed into a paid order (PAID), deleted on Shopify (CANCELLED - the sale is not happening), or still open (PENDING).
3 methods
NotifyActionclassA Shopify webhook, its HMAC and its shop checked. The order topics read as what they mean: orders/paid is PAID, orders/cancelled CANCELLED, refunds/create REFUNDED; the rest (fulfilments...) is handed back with no status, for the application to read from $raw.
3 methods
PurchaseActionclassA draft order, paid on Shopify's invoice page: the buyer is sent there (PENDING with the invoice URL), and Shopify's orders/paid webhook or fetch() says when they paid. A draft order's lines take the prices they are given - a line with a variant gid as its reference names the variant, so Shopify counts its stock; any other is a custom line. The shipping and the discount are carried over whole. Shopify's checkout never redirects back: returnUrl is not used.
4 methods
AdminApiclassThe Admin GraphQL API of the configured shop: the client bound to one Endpoint.
5 methods
EndpointclassOne Shopify shop: where it is, which API version to speak, and its tokens.
7 methods · 5 properties
GraphQLclassPOST a GraphQL document, get the `data` back or a ShopifyApiException.
3 methods
HmacclassShopify signs a webhook body with the app's API secret and sends the digest in X-Shopify-Hmac-Sha256: base64 of the raw binary hash, one value, no timestamp (X-Shopify-Triggered-At is outside the signature). Replay defence is the delivery id, X-Shopify-Webhook-Id, which the application keeps.
2 methods
ShopifyApiExceptionclassShopify said no, or said nothing. Carries the userErrors/errors it came with; never the access token (the headers are built at request time).
2 methods · 1 property
Omnitrade\Shopify
Api class
final class Api Api.php:12
One shop's APIs, for the gateway's actions.
public readonly AdminApi $adminpublic readonly Endpoint $endpoint
Api::__construct()
public function __construct(public readonly Endpoint $endpoint, HttpClientInterface $http, public readonly array $draftOrderTags = [], ?LoggerInterface $logger = null, float $retryDelay = 1.0)Documents class
final class Documents Documents.php:6
The GraphQL documents the actions send.
const DRAFT_ORDER_CREATE = <<<'GRAPHQL' mutation DraftOrderCreate($input: DraftOrderInput!) { draftOrderCreate(input: $input) { draftOrder { id name invoiceUrl status totalPrice …const DRAFT_ORDER = <<<'GRAPHQL' query DraftOrder($id: ID!) { draftOrder(id: $id) { id name status invoiceUrl totalPrice order { id name displayFinancialStatus display…const ORDER = <<<'GRAPHQL' query Order($id: ID!) { order(id: $id) { id name createdAt displayFinancialStatus displayFulfillmentStatus totalPriceSet { shopMoney {…const PRODUCT_FIELDS = <<<'GRAPHQL' fragment ProductFields on Product { id handle title descriptionHtml vendor productType status tags updatedAt onlineStoreUrl options { name o…What the catalogue reads of a product: its texts, vendor, type, collections, options, metafields, media and variants (100 at most, Shopify's former limit - a product with more is read in part).
const PRODUCTS = <<<'GRAPHQL' query Products($first: Int!, $after: String, $query: String) { shop { currencyCode taxesIncluded } products(first: $first, after: $after, qu…A page of products, the most recently updated last. The shop's currency and tax setting come along: the prices are in it. first: 50 by default rather than the permitted 250 - the query is costed by the nodes it can return (products x variants), and the cost bucket is waited for.
const PRODUCT = <<<'GRAPHQL' query Product($id: ID!) { shop { currencyCode taxesIncluded } product(id: $id) { ...ProductFields } } GRAPHQL . "\n" . self::PRODUCT_FIELDSconst SHOP = <<<'GRAPHQL' query Shop { shop { currencyCode taxesIncluded } } GRAPHQLThe shop's currency and tax setting, for a webhook's prices (they come bare).
const VARIANT_STOCKS = <<<'GRAPHQL' query VariantStocks($ids: [ID!]!) { nodes(ids: $ids) { ... on ProductVariant { id sku inventoryQuantity inventoryItem { id tracked } } }…The variants' inventory, by their gids.
const STOCKS = <<<'GRAPHQL' query Stocks($first: Int!, $after: String) { productVariants(first: $first, after: $after) { pageInfo { hasNextPage endCursor } nodes …Every variant's inventory, page by page.
const REFERENCE_KEY = 'omnitrade_reference'The custom attribute both sides correlate on: the merchant's reference on the draft order.
Products class
final class Products Products.php:31
Shopify's two product shapes, read as one Product.
Shopify sends two different shapes for the same product and does not warn you:
fromGraphQL() what the products(...) and product(id:) queries return - "id" is a gid, variants are under variants.nodes, prices are decimal strings, the fields are camelCase. fromRest() what the products/create|update webhook POSTs - "id" is a bare number, "variants" is a flat array, the fields are snake_case, the tags one comma-separated string.
The webhook payload is REST-shaped even when the subscription was created through GraphQL, so both converge here, and the tests assert they give the same Product for the same product. Pure: arrays in, models out.
Products::fromGraphQL()
public static function fromGraphQL(array $node, string $currency, ?bool $taxIncluded = null): Product$nodea products(...).nodes[] entry, or product(id:)
$taxIncludedthe shop's taxesIncluded
Products::fromRest()
public static function fromRest(array $payload, string $currency, ?bool $taxIncluded = null): Product$payloada products/create|update webhook body
Products::stock()
public static function stock(array $node): StockA variant's inventory, as VARIANT_STOCKS and STOCKS return it.
Products::status()
public static function status(mixed $status): string"ACTIVE", "active" -> Product::ACTIVE; DRAFT, UNLISTED -> DRAFT; ARCHIVED.
ShopifyGatewayFactory class
final class ShopifyGatewayFactory extends GatewayFactory ShopifyGatewayFactory.php:35
Shopify, a commerce platform: a sale paid on the shop's invoice page (a draft order), the shop's orders, its catalogue (products, variants, inventory) and its webhooks.
options: shop_domain: '%env(SHOPIFY_SHOP_DOMAIN)%' # example.myshopify.com admin_token: '%env(SHOPIFY_ADMIN_TOKEN)%' # shpat_..., a custom app's Admin API token webhook_secret: '%env(SHOPIFY_WEBHOOK_SECRET)%' # the app's API secret, for notify() api_version: '2026-07' timeout: 15 draft_order_tags: [] # tags put on the draft orders it creates currency: null # the catalogue's prices' currency; null: the shop's retry_delay: 1 # seconds before retrying a 429 / 5xx (doubled each time, 3 attempts); a 429's Retry-After prevails
No authorizations, captures nor refunds from here: Shopify's own admin does those on its orders.
Options
| Option | Default | |
|---|---|---|
webhook_secret | null | |
api_version | '2026-07' | |
timeout | 15 | |
draft_order_tags | [] | |
currency | null | |
retry_delay | 1 | |
omnitrade.api | function (Config $c) {
$http = $this->http ?? HttpClient::create();
return new Api(new Endpoint($c['shop_domain'], $c['api_version'], $c['admin_token… | |
omnitrade.action.purchase | new PurchaseAction() | |
omnitrade.action.fetch | new FetchTransactionAction() | |
omnitrade.action.order | new FetchOrderAction() | |
omnitrade.action.notify | static fn(Config $c) => new NotifyAction($c['currency'] ?: null) | |
omnitrade.action.products | static fn(Config $c) => new FetchProductsAction($c['currency'] ?: null) | |
omnitrade.action.product | static fn(Config $c) => new FetchProductAction($c['currency'] ?: null) | |
omnitrade.action.inventory | new FetchInventoryAction() | |
shop_domain | required | |
admin_token | required |
Omnitrade\Shopify\Action
FetchInventoryAction class
final class FetchInventoryAction implements ActionInterface, ApiAwareInterface FetchInventoryAction.php:21
The variants' inventory: inventoryQuantity (summed over the locations) when inventoryItem.tracked, else a null quantity. The variants by their gids (or bare ids) through nodes(ids:), 250 at a time; none given, every variant of the shop through productVariants, page by page.
FetchInventoryAction::__construct()
public function __construct()FetchInventoryAction::supports()
public function supports(Request $request): boolFetchInventoryAction::execute()
public function execute(Request $request): voidFetchOrderAction class
final class FetchOrderAction implements ActionInterface, ApiAwareInterface FetchOrderAction.php:21
A Shopify order, by its gid or its bare id.
FetchOrderAction::__construct()
public function __construct()FetchOrderAction::supports()
public function supports(Request $request): boolFetchOrderAction::execute()
public function execute(Request $request): voidFetchOrderAction::order()
public static function order(array $order): PlatformOrder$orderas Documents::ORDER returns it
FetchProductAction class
final class FetchProductAction implements ActionInterface, ApiAwareInterface FetchProductAction.php:23
One product: by its gid, its bare numeric id, its handle, or the address of a page of it - the storefront's /products/<handle> (the handle is the last segment) or the admin's /admin/products/<id>. Null when the shop has no such product.
FetchProductAction::__construct()
public function __construct(readonly ?string $currency = null)$currencythe prices' currency; null: the shop's
FetchProductAction::supports()
public function supports(Request $request): boolFetchProductAction::execute()
public function execute(Request $request): voidFetchProductAction::target()
public static function target(Reference $reference): array- Returns
the gid, or the handle
FetchProductsAction class
final class FetchProductsAction implements ActionInterface, ApiAwareInterface FetchProductsAction.php:22
A page of the shop's products, by the Admin GraphQL products(...) query, the least recently updated first: the cursor is Shopify's endCursor, updatedSince becomes the search filter updated_at:>'...' and the query is appended to it as Shopify's own search text. Pass the same updatedSince and query with the cursor: Shopify's cursor is only a position.
FetchProductsAction::__construct()
public function __construct(readonly ?string $currency = null)$currencythe prices' currency; null: the shop's
FetchProductsAction::supports()
public function supports(Request $request): boolFetchProductsAction::execute()
public function execute(Request $request): voidFetchProductsAction::search()
public static function search(?\DateTimeInterface $updatedSince, ?string $query): ?stringShopify's search syntax: "updated_at:>'2026-10-01T00:00:00Z' margaux".
FetchTransactionAction class
final class FetchTransactionAction implements ActionInterface, ApiAwareInterface FetchTransactionAction.php:21
What became of a draft order: completed into a paid order (PAID), deleted on Shopify (CANCELLED - the sale is not happening), or still open (PENDING).
This is what covers a buyer who comes back before the webhook, or a webhook that never arrived.
FetchTransactionAction::__construct()
public function __construct()FetchTransactionAction::supports()
public function supports(Request $request): boolFetchTransactionAction::execute()
public function execute(Request $request): voidNotifyAction class
final class NotifyAction implements ActionInterface, ApiAwareInterface NotifyAction.php:39
A Shopify webhook, its HMAC and its shop checked. The order topics read as what they mean: orders/paid is PAID, orders/cancelled CANCELLED, refunds/create REFUNDED; the rest (fulfilments...) is handed back with no status, for the application to read from $raw.
The catalogue topics carry the catalogue: products/create and products/update the Product (the REST payload read like a GraphQL node), products/delete its gid alone (no product: it is gone), and inventory_levels/update one Stock - the level at one location, which names the inventory item rather than the variant: Stock::$reference and $item are then the InventoryItem's gid, to match on the variant's Stock::$item.
The reference is the draft order the sale began with (the transaction's reference) when the order names one, else the order's own gid; the merchant's reference travels in the note attributes (Documents::REFERENCE_KEY).
NotifyAction::__construct()
public function __construct(readonly ?string $currency = null)$currencythe prices' currency; null: the shop's, asked once
NotifyAction::supports()
public function supports(Request $request): boolNotifyAction::execute()
public function execute(Request $request): voidPurchaseAction class
final class PurchaseAction implements ActionInterface, ApiAwareInterface PurchaseAction.php:27
A draft order, paid on Shopify's invoice page: the buyer is sent there (PENDING with the invoice URL), and Shopify's orders/paid webhook or fetch() says when they paid. A draft order's lines take the prices they are given - a line with a variant gid as its reference names the variant, so Shopify counts its stock; any other is a custom line. The shipping and the discount are carried over whole. Shopify's checkout never redirects back: returnUrl is not used.
PurchaseAction::__construct()
public function __construct()PurchaseAction::supports()
public function supports(Request $request): boolPurchaseAction::execute()
public function execute(Request $request): voidPurchaseAction::input()
public static function input(Payment $payment, array $tags = []): array- Returns
a DraftOrderInput
Omnitrade\Shopify\Api
AdminApi class
class AdminApi AdminApi.php:6
The Admin GraphQL API of the configured shop: the client bound to one Endpoint.
AdminApi::__construct()
public function __construct(readonly GraphQL $graphql, readonly Endpoint $endpoint)AdminApi::endpoint()
public function endpoint(): EndpointAdminApi::isConfigured()
public function isConfigured(): boolAdminApi::query()
public function query(string $document, array $variables = []): array- Throws
ShopifyApiException
AdminApi::mutate()
public function mutate(string $document, array $variables, string $root): arrayA mutation, with its userErrors raised rather than returned.
Endpoint class
final class Endpoint Endpoint.php:10
One Shopify shop: where it is, which API version to speak, and its tokens.
Every argument is nullable and normalised here, so an unset environment variable (null, not '') degrades to "not configured" instead of a type error.
public readonly string $shopDomainpublic readonly string $apiVersionpublic readonly string $adminTokenpublic readonly string $webhookSecretpublic readonly int $timeout
Endpoint::__construct()
public function __construct(?string $shopDomain = '', ?string $apiVersion = '2026-07', ?string $adminToken = '', ?string $webhookSecret = '', ?int $timeout = 15)Endpoint::isConfigured()
public function isConfigured(): boolWhether there is enough here to call the Admin API at all.
Endpoint::canVerifyWebhooks()
public function canVerifyWebhooks(): boolEndpoint::shop()
public function shop(): stringThe shop as Shopify names it in X-Shopify-Shop-Domain.
Endpoint::adminUrl()
public function adminUrl(): stringEndpoint::adminLink()
public function adminLink(string $gid): stringThe admin URL of one resource, for an "open in Shopify" link.
Endpoint::gid()
public static function gid(string $type, string|int $id): string"gid://shopify/Order/123" from a bare id, or the gid as given.
GraphQL class
class GraphQL GraphQL.php:20
POST a GraphQL document, get the data back or a ShopifyApiException.
Knows nothing about orders or products: every document is passed in. It knows two things about Shopify: the Admin API is a leaky bucket costed per query (extensions.cost.throttleStatus), waited for when it runs low; and a THROTTLED answer is retried with backoff. The access token never reaches a log or an exception: not its message, its previous (a copy of the HTTP client's, Redaction::copy()) nor its trace's arguments.
GraphQL::__construct()
public function __construct(readonly HttpClientInterface $http, readonly ?LoggerInterface $logger = null, ?callable $sleep = null, readonly float $retryDelay = 1.0)GraphQL::query()
public function query(string $url, array $headers, string $document, array $variables = [], int $timeout = 15): array- Returns
the
dataobject- Throws
ShopifyApiException
GraphQL::assertNoUserErrors()
public function assertNoUserErrors(string $what, array $result): arrayRaise a mutation's own userErrors: Shopify answers 200 with them when it refuses on business grounds.
Hmac class
final class Hmac Hmac.php:11
Shopify signs a webhook body with the app's API secret and sends the digest in X-Shopify-Hmac-Sha256: base64 of the raw binary hash, one value, no timestamp (X-Shopify-Triggered-At is outside the signature). Replay defence is the delivery id, X-Shopify-Webhook-Id, which the application keeps.
Hmac::verify()
public static function verify(string $payload, ?string $header, string $secret): boolHmac::sign()
public static function sign(string $payload, string $secret): stringThe header Shopify would send for this body - used by the tests.
ShopifyApiException class
class ShopifyApiException extends ProviderException ShopifyApiException.php:11
Shopify said no, or said nothing. Carries the userErrors/errors it came with; never the access token (the headers are built at request time).
public readonly array $errors
ShopifyApiException::__construct()
public function __construct(string $message, public readonly array $errors = [], ?\Throwable $previous = null)ShopifyApiException::fromErrors()
public static function fromErrors(string $what, array $errors): self