Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Shopify Graphql Laravel Package

luminarix/laravel-shopify-graphql

Laravel Shopify GraphQL offers a fluent client for Shopify’s GraphQL Admin API, with query and mutation builders, pagination, rate-limit handling, and Laravel-ready configuration for building Shopify apps and integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up credentials

composer require luminarix/laravel-shopify-graphql
php artisan vendor:publish --provider="LuminariX\Shopify\GraphQL\ShopifyGraphQLServiceProvider"

The service provider is auto-discovered, so there is nothing to register in config/app.php. Publishing gives you config/shopify-graphql.php; that file is the first thing to open, because every other step depends on the key names it actually ships with (shop domain, access token, API version, and any timeout/retry knobs). Then set the corresponding env vars in .env rather than editing the config:

SHOPIFY_SHOP_DOMAIN=my-dev-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxx
SHOPIFY_API_VERSION=2025-01

Getting a token. In the Shopify admin go to Settings → Apps and sales channels → Develop apps → Create an app, configure Admin API scopes (read_products, write_products, …), install it, and copy the Admin API access token. This package talks to the Admin GraphQL API, so a Storefront token or a legacy private-app password will not work. Note the token is shown once — store it immediately.

First call. Open a php artisan tinker session and run the smallest possible query to prove auth, domain, and version are all correct:

use LuminariX\Shopify\GraphQL\Facades\ShopifyGraphQL;

ShopifyGraphQL::query(<<<'GQL'
{
  shop {
    name
    myshopifyDomain
    currencyCode
  }
}
GQL)->json();

If shop.name comes back, you are done with setup. If you get a 401/403, the token or scopes are wrong; if you get "Field 'shop' doesn't exist", your api_version is malformed (see Gotchas).

Where to look next, in order: the published config file → the facade class (to confirm the public method names — query, mutation, and any ->variables()/->json() helpers) → the client class itself (it is small; reading it once tells you exactly how errors and pagination are surfaced).

Implementation Patterns

Bind the client, don't sprinkle the facade. The facade is convenient in tinker and in one-off artisan commands, but in application code inject the underlying client (or a thin ShopifyService of your own that wraps it). This keeps you testable and makes the multi-tenant case below tractable.

final class ShopifyCatalog
{
    public function __construct(private \LuminariX\Shopify\GraphQL\ShopifyGraphQL $shopify) {}

    public function productByHandle(string $handle): ?array
    {
        $res = $this->shopify->query(<<<'GQL'
        query ProductByHandle($query: String!) {
          products(first: 1, query: $query) {
            edges { node { id title handle status } }
          }
        }
        GQL, ['query' => "handle:{$handle}"])->json();

        return $res['data']['products']['edges'][0]['node'] ?? null;
    }
}

Always use GraphQL variables, never string interpolation. Shopify handles are user-supplied and show up inside search strings; interpolating them is an injection and encoding hazard in one. The query: "handle:…" form above is the idiomatic Shopify search-filter syntax and is safe because the value is bound, not concatenated.

Mutations: check userErrors, not just HTTP status. Shopify returns HTTP 200 for business-rule failures. A mutation "succeeds" at the transport layer while doing nothing.

$res = $shopify->mutation(<<<'GQL'
mutation CreateProduct($input: ProductInput!) {
  productCreate(input: $input) {
    product { id }
    userErrors { field message }
  }
}
GQL, ['input' => ['title' => $title, 'status' => 'DRAFT']])->json();

$errors = $res['data']['productCreate']['userErrors'] ?? [];
if ($errors !== []) {
    throw new ShopifyMutationException($errors); // log field+message, not the whole payload
}

Pagination: cursor loop on pageInfo. The package exposes helpers, but the reliable pattern is to drive it yourself so you control page size and stop conditions.

$cursor = null;
do {
    $page = $shopify->query($paginationQuery, ['first' => 100, 'after' => $cursor])->json();
    $conn = $page['data']['products'];
    foreach ($conn['edges'] as $edge) { $rows[] = $edge['node']; }
    $cursor = $conn['pageInfo']['endCursor'];
} while ($conn['pageInfo']['hasNextPage']);

Cap the loop (max 50 pages) so a runaway cursor bug can't hammer the API.

Bulk extraction. For anything over a few thousand records, do not paginate — use Shopify's bulkOperationRunQuery mutation, poll the bulk operation status, then download the returned JSONL URL. This is the single largest performance lever in the Shopify API and the wrapper doesn't hide it from you.

Testing. Fence the HTTP layer with Laravel's Http::fake() and assert on the URL and the outgoing GraphQL body. Fake both healthy responses and the {"errors":[{"message":"Throttled"}]} shape so your retry logic is exercised. Because the client resolves from the container, you can also swap in a stub implementation to test callers without touching HTTP at all.

Queue everything that writes. Mutations are slow and rate-limited; run them inside jobs with $tries = 5 and an exponential backoff. Keep the read path synchronous only for tiny, user-facing lookups.

Gotchas and Tips

HTTP 200 does not mean success. GraphQL returns a top-level errors array for schema/validation/throttling problems while the status code stays 200. Every call site should check $res['errors'] ?? [] before touching $res['data']. Wrapping this check once in your own service method is the highest-value thing you can do with this package.

API version strings are picky. Shopify requires YYYY-MM with a zero-padded month (2025-01, not 2025-1) and only supports a rolling ~12-month window. A stale version in .env is the most common cause of sudden Field ... doesn't exist errors, and it will break silently on Shopify's quarterly release schedule. Pin the version explicitly in config and put a calendar reminder on it.

GIDs are strings, not integers. Every ID is gid://shopify/Product/1234567890. Don't cast to int, don't compare against a database id column, and store the full GID if you persist it. Parse with a regex or basename() when you need the numeric tail.

Money is a string, quantity is an int, and nulls are everywhere. price/amount come back as decimal strings ("19.99") alongside a currencyCode — use brick/money or bcmath, never (float) on currency. Fields you request can legitimately be null; treat every key as optional in your DTO mapping.

Throttling is cost-based, not request-count based. Shopify uses a leaky bucket measured in query cost, exposed at extensions.cost.throttleStatus.currentlyAvailable. Cheap queries with wide field selections can cost far more than narrow ones. On THROTTLED, sleep based on throttleStatus.restoreRate and retry — do not retry immediately in a tight loop. Also, first:/last: are required on every connection and capped at 250; asking for more is a validation error.

Multi-tenant stores: don't mutate global config. If you serve several shops from one app, a singleton client bound with a single token will leak credentials between tenants faster than you'd expect. Build a per-shop client instance (or a forShop($domain, $token) factory around the underlying class) and inject that, rather than calling config()/Config::set() at request time — which also silently breaks under php artisan config:cache.

Don't log the payload wholesale. Request/response logging is tempting for debugging, but responses echo customer PII and the request carries the X-Shopify-Access-Token header. Log GraphQL operation names and errors[].message only; redact the token header in any exception handler.

Maturity caveat. With a handful of stars and no dependents, this package has a small surface area and an equally small support base — assume the public API is exactly what you read in the source, not what you infer from the README. Because the client is thin, budget half a day to read its ~few hundred lines; if it proves awkward, the escape hatch is trivial, since the whole thing is just an authenticated Guzzle POST to https://{shop}.myshopify.com/admin/api/{version}/graphql.json. Keeping your own ShopifyService wrapper in front of it means you can swap the underlying transport without touching call sites — which is the real reason to inject rather than facade-alias it everywhere.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource