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 Laravel Package

kyon147/laravel-shopify

A maintained full-featured Laravel package for Shopify app development, with OAuth, webhooks, billing, and support for expiring offline access tokens. Works with Laravel 8+ and installs via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

This is the maintained fork of the (now deprecated) osiset/laravel-shopify. It is not a thin API wrapper — it ships the whole OAuth install flow, webhook registration, billing, and the admin routes an embedded Shopify app needs. Budget an afternoon for install, not five minutes.

Minimum viable install:

composer require kyon147/laravel-shopify
php artisan vendor:publish --tag=shopify-config
php artisan vendor:publish --tag=shopify-migrations   # confirm the exact tag in the wiki
php artisan migrate

Then set at least these in .env (names mirror the published config/shopify-app.php, which is the file you should open first):

  • SHOPIFY_API_KEY / SHOPIFY_API_SECRET — from the Partners dashboard app
  • SHOPIFY_API_SCOPES — comma-separated; request the minimum, changes force re-auth
  • SHOPIFY_API_VERSION — pin it explicitly, never rely on a default
  • SHOPIFY_API_REDIRECT — must match the Partners dashboard "App URL"/redirect exactly
  • APP_URL — https, publicly reachable (ngrok/cloudflared locally)

Where to look first, in order:

  1. config/shopify-app.php — this file is the documentation. Routes, middleware, webhooks, billing plans, and the user_model binding all live here.
  2. The wiki: Installation → Route List → Usage. The Route List page is the one most people miss; it tells you which routes the package already owns so you don't collide with them.
  3. php artisan route:list | grep -i shopify — faster than reading config to see what got registered.

First use case — prove the loop end-to-end: install the app on a dev store, land on the package's home route, then in a controller do:

$shop = ShopifyApp::shop();

$response = $shop->api()->graph('{ shop { name myshopifyDomain currencyCode } }');

If that returns data, OAuth, token storage, HMAC verification, and the API client are all wired. Everything else is incremental.

If you are building a new public app, decide on expiring offline tokens now, before you have customers:

SHOPIFY_EXPIRING_OFFLINE_TOKENS=true

Shopify requires expiring offline tokens for public apps created on/after 2026-04-01. Retrofitting later is a one-way migration per shop.

Implementation Patterns

Treat the shops table as your tenant table. By default it doubles as config('shopify-app.user_model') — it holds shop domain, token, plan, and the usual user-ish columns. For per-user (multi-tenant) apps, put the ShopModel trait on your own User model and point user_model at it, so credentials hang off your existing auth rather than a parallel table.

Two API surfaces, and you should prefer one. apiHelper() builds the client; $shop->api() is the shortcut:

// GraphQL — prefer this
$shop->api()->graph($query, $variables);

// REST — legacy Admin API, being sunset; use for one-off endpoints only
$shop->api()->rest('GET', '/admin/api/2025-01/orders.json', ['limit' => 50]);

REST calls need the version embedded in the path and are not automatically kept in sync with SHOPIFY_API_VERSION. GraphQL calls are. Route new work through graph().

Never pass an Eloquent model into a queued job and trust it. Pass $shop->id (or the domain), then re-resolve in handle():

public function handle(): void
{
    $shop = Shop::findOrFail($this->shopId);
    $shop->api()->graph($mutation, ['input' => [...]]);
}

Webhooks: declare in config, handle in jobs. Map topics to job classes that extend the package's WebhookJob:

'api_webhooks' => [
    'orders/create' => \App\Jobs\OrdersCreate::class,
    'app/uninstalled' => \App\Jobs\AppUninstalled::class,
],

Register the webhook route behind the package's webhook-verification middleware, keep the handler fast (verify → persist raw payload → dispatch), and make handlers idempotent — Shopify retries and can deliver out of order. If you change api_webhooks after install, the store won't know about it until you re-register; the usual fix is re-running the install/auth flow on a dev store.

Gate premium features with billing in config, not in controllers. Define plans under api_billing_*, put the package's Billable middleware on your premium route group, then check $shop->plan in views. This keeps the charge/redirect dance out of your own code and survives Shopify's billing-flow changes.

Protect your routes deliberately. Two different worlds:

  • Embedded (App Bridge / iframe): verify the session token on every request. Third-party cookie blocking in Safari/iOS makes cookie-based auth unreliable — session tokens are the only durable path.
  • Standalone/full-page: HMAC verification on the query string is sufficient.

Mixing the two is the most common source of redirect loops. Pick one per route group and stick to it.

Use events for install lifecycle. The package fires authenticated/installed/uninstalled events; hook listeners rather than stuffing logic into routes. Uninstall in particular must clear local state — the token is dead the moment the webhook arrives.

In tests, swap the shop's API client for a fake rather than hitting Shopify. Resolve shops by ID in your code and the seams stay clean.

Gotchas and Tips

APP_KEY is now load-bearing. Offline refresh tokens are stored via Laravel's encrypter. Rotate APP_KEY carelessly and every shop's refresh token becomes undecryptable. Treat key rotation as a migration project, not a .env edit.

Expiring-token migration is one-way. When you exchange a legacy non-expiring offline token for an expiring one, Shopify revokes the old token. Test on a throwaway dev store first. The passive auto-migrate path (SHOPIFY_AUTO_MIGRATE_LEGACY, default on) runs on the first apiHelper() call and fails open — it logs and proceeds with the legacy token, so a silent log line is your only signal. Watch logs during rollout, or use the explicit action MigrateShopToExpiringOfflineAccessToken and inspect its migrated/skipped/reason/error keys.

Long-running jobs hold stale tokens. The API client is cached when first built. A job that loops for hours keeps the expired token even after refresh logic would have fired. Either enable SHOPIFY_REFRESH_OFFLINE_TOKEN_BEFORE_API_CALL=true (re-checks before each api()/apiHelper()) or do it manually per iteration:

if (! $shop->offlineAccessTokenIsFresh()) {
    $shop->refreshOfflineAccessTokenIfNeeded();
}
$shop->api()->graph('...');

$shop->resetApiClient() clears the cached client if you need a forced rebuild.

Merge your casts, don't overwrite them. If your User/Shop model declares $casts, the ShopModel trait needs datetime casts for the two *_expires_at columns. The package uses mergeCasts during initializeShopModel — don't defeat it with a hard-coded array that predates the trait.

Serverless (Vapor, Lambda): use the queue commands, not the loops. shopify-app:migrate-expiring-offline-tokens and shopify-app:refresh-expiring-offline-tokens dispatch jobs rather than doing HTTP inline. Both support --dry-run, --shop=, --queue=, --connection= — point them at a dedicated worker via SHOPIFY_MIGRATE_OFFLINE_TOKENS_JOB_QUEUE etc. Note the package does not schedule anything; add to routes/console.php:

Schedule::command('shopify-app:refresh-expiring-offline-tokens')->daily();

Refresh tokens lapse at ~90 days of inactivity, so dormant shops will silently die without this.

The API route now throws if opened in a browser (v27.1.0). Curling it without proper headers will look like a bug and isn't.

The underlying HTTP client is the weakest link. OAuth/token exchange has been decoupled from gnikyt/basic-shopify-api, but REST/Graph traffic still routes through it. If you need long-term maintenance guarantees, plan to fork or replace that layer — the README says as much outright.

Local dev checklist before blaming the package: https + public URL, redirect URI byte-identical to the dashboard, correct API version, scopes that include what you're calling, and a freshly-installed shop after any webhook/scope config change.

Version pinning matters. v18 dropped PHP 7, v26 dropped older PHP/Laravel, v27 changed token semantics. Read the upgrade guide in the wiki before a major bump — the releases are small in number but large in scope.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package