PHPackages                             amadulhaque/shopify-bridge - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. amadulhaque/shopify-bridge

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

amadulhaque/shopify-bridge
==========================

A contract-driven Shopify package for Laravel.

v1.1.5(today)011↑2900%MITPHPPHP ^8.3

Since Jul 30Pushed todayCompare

[ Source](https://github.com/AmadulHaque/shopify-bridge)[ Packagist](https://packagist.org/packages/amadulhaque/shopify-bridge)[ Docs](https://github.com/amadulhaque/shopify-bridge)[ RSS](/packages/amadulhaque-shopify-bridge/feed)WikiDiscussions main Synced today

READMEChangelog (7)Dependencies (4)Versions (8)Used By (0)

Amadulhaque Laravel Shopify
===========================

[](#amadulhaque-laravel-shopify)

A framework-style Shopify integration for Laravel. It owns Shopify protocol work—OAuth, Admin GraphQL, webhook subscriptions, and HMAC verification—while the host application owns shops, users, tokens, persistence, tenancy, and authorization.

It has **no** models, migrations, database access, sessions, queues, cache stores, Redis dependency, authentication guards, or storage implementation.

Contents
--------

[](#contents)

- [Install](#install)
- [Configure](#configure)
- [Bind application contracts](#bind-application-contracts)
- [OAuth install flow](#oauth-install-flow)
- [GraphQL usage](#graphql-usage)
- [Middleware](#middleware)
- [Webhooks](#webhooks)
- [Events](#events)
- [Testing](#testing)

Install
-------

[](#install)

Install the package from Packagist:

```
composer require amadulhaque/shopify-bridge
```

Laravel discovers `ShopifyServiceProvider` automatically. Publish the configuration only when you need to customize it:

```
php artisan vendor:publish --tag=shopify-config
```

For this repository's local development package, Composer already uses `packages/Shopify` as a path repository, so no additional install command is needed.

Configure
---------

[](#configure)

Set these values in the host application's `.env`:

```
SHOPIFY_CLIENT_ID=
SHOPIFY_CLIENT_SECRET=
SHOPIFY_REDIRECT_URI=https://app.example.com/shopify/callback
# Redis, file, database, etc. Omit to use Laravel CACHE_STORE.
SHOPIFY_STATE_CACHE_STORE=redis
SHOPIFY_API_VERSION=2026-07
SHOPIFY_SCOPES=read_products,write_products
```

Add the same complete callback URL to your Shopify app's allowed redirection URLs. The published `config/shopify.php` configures API version, scopes, retries, timeout, and a replaceable HTTP client. It does not configure storage.

### OAuth state data

[](#oauth-state-data)

Pass optional state data as third `redirect()` or `authorizationUrl()` argument. Package sends only an opaque random state token to Shopify, stores your data through `OAuthStateRepository`, then returns it as `$result->state` after valid callback. Do not JSON-encode or URL-encode state yourself.

```
return Shopify::oauth()->redirect($shop, route('shopify.callback'), [
    'id' => 1234,
]);

// After exchange:
$result->state['id'];
```

Bind application contracts
--------------------------

[](#bind-application-contracts)

OAuth state uses package cache repository by default. Set `SHOPIFY_STATE_CACHE_STORE=redis` or `file` (or omit it to use Laravel `CACHE_STORE`). Bind your own `OAuthStateRepository` only when custom state storage is needed. Bind your own implementations for tokens, shop resolution, webhook definitions, and application authorization.

```
// app/Providers/AppServiceProvider.php

use App\Shopify\MongoOAuthStateRepository;
use App\Shopify\SubdomainShopResolver;
use App\Shopify\TenantTokenRepository;
use App\Shopify\TenantWebhookRepository;
use Amadulhaque\Shopify\Contracts\OAuthStateRepository;
use Amadulhaque\Shopify\Contracts\ShopResolver;
use Amadulhaque\Shopify\Contracts\TokenRepository;
use Amadulhaque\Shopify\Contracts\WebhookRepository;

public function register(): void
{
    $this->app->bind(OAuthStateRepository::class, MongoOAuthStateRepository::class);
    $this->app->bind(TokenRepository::class, TenantTokenRepository::class);
    $this->app->bind(ShopResolver::class, SubdomainShopResolver::class);
    $this->app->bind(WebhookRepository::class, TenantWebhookRepository::class);
}
```

`OAuthStateRepository::put()` receives optional state data. `pull()` must return it and consume state once. `TokenRepository::findFor(Shop $shop)` must return `AccessToken|null`. The package never calls a `save`, `create`, or `update` method on a repository.

### Optional application authorization

[](#optional-application-authorization)

Bind `AuthorizationResolver` when your application needs to decide whether its current caller may access a resolved shop. It can use Laravel Auth, a JWT, an API key, or any custom tenant system. If it is not bound, `shopify.auth` only verifies that a shop and Shopify token resolve.

```
use App\Shopify\CurrentUserShopAuthorization;
use Amadulhaque\Shopify\Contracts\AuthorizationResolver;

$this->app->bind(AuthorizationResolver::class, CurrentUserShopAuthorization::class);
```

OAuth install flow
------------------

[](#oauth-install-flow)

The package does not register routes because URL structure is an application decision. Define your own routes:

```
use App\Http\Controllers\ShopifyInstallController;
use Illuminate\Support\Facades\Route;

Route::get('/shopify/install', [ShopifyInstallController::class, 'install']);
Route::get('/shopify/callback', [ShopifyInstallController::class, 'callback'])
    ->name('shopify.callback');
```

Example controller. `ShopifyInstallationStore` below is an **application** service; define it however your product stores installations.

```
namespace App\Http\Controllers;

use App\Shopify\ShopifyInstallationStore;
use Amadulhaque\Shopify\Facades\Shopify;
use Illuminate\Http\Request;

class ShopifyInstallController
{
    public function install(Request $request)
    {
        return Shopify::oauth()->redirect($request->string('shop')->toString());
    }

    public function callback(Request $request, ShopifyInstallationStore $installations)
    {
        $result = Shopify::oauth()->exchange($request);

        // Your application decides where and how this is persisted/encrypted.
        $installations->save($result->shop, $result->token);

        return to_route('dashboard');
    }
}
```

```
Merchant -> Host app: GET /shopify/install?shop=demo.myshopify.com
Host app -> OAuthStateRepository: put(random state, shop, expiry)
Host app -> Shopify: redirect to authorization URL
Shopify -> Host app: callback(code, state, hmac, shop)
Host app -> OAuthStateRepository: pull(state) [one time]
Host app -> Shopify: exchange code for token
Host app -> Application store: save(shop, token)

```

`exchange()` returns an immutable `OAuthResult` with `Shop` and `AccessToken`; it never persists either value.

### Providing and validating an existing access token

[](#providing-and-validating-an-existing-access-token)

For an already-installed shop, pass its domain and token directly. The token is used only for that call chain and is never persisted by this package.

```
$oauth = Shopify::oauth('amad123s.myshopify.com', 'shpat_existing_token');

if (! $oauth->accessTokenIsValid()) {
    // Reinstall or ask the merchant to reconnect the app.
}

// The configured shop makes the domain optional here.
$url = Shopify::oauth('amad123s.myshopify.com')->authorizationUrl();
```

GraphQL usage
-------------

[](#graphql-usage)

GraphQL resolves the access token from `TokenRepository`, so callers only provide a shop.

```
use Amadulhaque\Shopify\Facades\Shopify;

$response = Shopify::graph()
    ->shop('demo.myshopify.com')
    ->query('query ($id: ID!) { product(id: $id) { id title } }', [
        'id' => 'gid://shopify/Product/1',
    ]);

if ($response->hasErrors()) {
    return response()->json(['errors' => $response->errors()], 422);
}

return response()->json($response->data());
```

Mutations use the same fluent API:

```
Shopify::graph()
    ->shop($shop)
    ->mutation('mutation ($input: ProductInput!) { productCreate(input: $input) { product { id } userErrors { message } } }', [
        'input' => ['title' => 'New product'],
    ]);
```

When you have a token at call time, pass it as the second `graph()` argument instead of implementing `TokenRepository`:

```
Shopify::graph('amad123s.myshopify.com', 'shpat_existing_token')
    ->query('query { shop { name } }');
```

`GraphqlResponse` preserves Shopify `data` and `errors`. Transport failures throw `ShopifyHttpException`. A GraphQL throttle response throws `GraphqlThrottled`; queue jobs should release using `$exception->retryAfterSeconds` rather than immediately retrying.

To replace Laravel's HTTP client, bind `Amadulhaque\Shopify\Contracts\HttpClient` to your adapter. This is where applications can add tracing, circuit breaking, distributed rate limiting, or custom retry behavior.

Middleware
----------

[](#middleware)

The package provides three aliases:

```
Route::get('/shop', ShopController::class)->middleware('shopify.shop');
Route::get('/shop/data', DataController::class)->middleware('shopify.auth');
Route::post('/webhooks/shopify', ShopifyWebhookController::class)->middleware('shopify.webhook');
```

- `shopify.shop`: resolves a `Shop` via `ShopResolver` and attaches it as `shopify.shop`.
- `shopify.auth`: resolves the shop, requires a token from `TokenRepository`, and optionally invokes `AuthorizationResolver`.
- `shopify.webhook`: verifies Shopify's HMAC and attaches a typed `Webhook` as `shopify.webhook`.

An authenticated Laravel route can combine normal application auth with Shopify authorization without coupling the package to a User model:

```
Route::get('/settings', SettingsController::class)
    ->middleware(['auth', 'shopify.auth']);
```

Webhooks
--------

[](#webhooks)

Provide the desired subscriptions from your application's `WebhookRepository`, then manage subscriptions through GraphQL:

```
use Amadulhaque\Shopify\Facades\Shopify;
use Amadulhaque\Shopify\Webhooks\WebhookSubscription;

Shopify::webhooks()->register($shop, new WebhookSubscription(
    'orders/create',
    'https://app.example.com/webhooks/orders',
));

Shopify::webhooks()->delete($shop, $subscriptionId);
Shopify::webhooks()->sync($shop);       // Registers missing desired subscriptions.
Shopify::webhooks()->sync($shop, true); // Also removes stale remote subscriptions.
```

All application-webhook operations can receive a direct access token as their final argument. `all()` retrieves every page; `sync()` uses it too, so it detects stale subscriptions beyond Shopify's first 250 results.

```
$shop = 'amad123s.myshopify.com';
$token = 'shpat_existing_token';

Shopify::webhooks()->register($shop, new WebhookSubscription(
    'orders/create',
    'https://app.example.com/webhooks/orders',
), $token);

$firstPage = Shopify::webhooks()->list($shop, null, $token);
$subscriptions = Shopify::webhooks()->all($shop, $token);
Shopify::webhooks()->sync($shop, true, $token);
Shopify::webhooks()->delete($shop, $subscriptionId, $token);
```

`sync(..., true)` is explicitly opt-in because deleting a remote subscription can affect another component that manages the same store.

Webhook handling stays thin:

```
public function __invoke(Request $request)
{
    $webhook = $request->attributes->get('shopify.webhook');

    ProcessShopifyWebhook::dispatch($webhook);

    return response()->noContent();
}
```

```
Shopify -> Host app: headers + raw JSON body
Host app -> VerifyWebhook: validate HMAC and required headers
VerifyWebhook -> Request: attach shopify.webhook value object
VerifyWebhook -> Events: WebhookReceived, WebhookVerified
Host app -> Application logic: queue/process payload

```

Events
------

[](#events)

Listen in the host application for package events:

```
use Amadulhaque\Shopify\Events\ShopInstalled;
use Illuminate\Support\Facades\Event;

Event::listen(ShopInstalled::class, function (ShopInstalled $event) {
    // Persist the result, initialize a tenant, or enqueue onboarding work.
});
```

Available events: `ShopInstalled`, `AccessTokenUpdated`, `WebhookReceived`, `WebhookVerified`, and `ShopUninstalled`.

Testing
-------

[](#testing)

Bind in-memory implementations of the contracts and fake Shopify HTTP requests:

```
use Illuminate\Support\Facades\Http;

Http::fake([
    'https://demo.myshopify.com/admin/api/*/graphql.json' => Http::response([
        'data' => ['shop' => ['name' => 'Demo']],
    ]),
]);
```

The package test suite covers OAuth exchange, token resolution, GraphQL requests, middleware aliases, HMAC rejection and verification, webhook events, the facade, and webhook registration. Run it with:

```
php artisan test
```

Public API
----------

[](#public-api)

```
Shopify::oauth()->redirect($shop);
Shopify::oauth()->exchange($request);
Shopify::oauth($shop, $accessToken)->accessTokenIsValid();
Shopify::graph()->shop($shop)->query($query, $variables);
Shopify::graph($shop, $accessToken)->query($query, $variables);
Shopify::graph()->shop($shop)->mutation($mutation, $variables);
Shopify::webhooks()->register($shop, $subscription);
Shopify::webhooks()->list($shop);
Shopify::webhooks()->all($shop);
Shopify::webhooks()->delete($shop, $subscriptionId);
Shopify::webhooks()->sync($shop);
Shopify::client();
```

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

7

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/92516695?v=4)[Amadul Haque](/maintainers/AmadulHaque)[@AmadulHaque](https://github.com/AmadulHaque)

---

Top Contributors

[![AmadulHaque](https://avatars.githubusercontent.com/u/92516695?v=4)](https://github.com/AmadulHaque "AmadulHaque (23 commits)")

---

Tags

laravelgraphqloauthwebhooksshopify

### Embed Badge

![Health badge](/badges/amadulhaque-shopify-bridge/health.svg)

```
[![Health](https://phpackages.com/badges/amadulhaque-shopify-bridge/health.svg)](https://phpackages.com/packages/amadulhaque-shopify-bridge)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M348](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77922.3M192](/packages/laravel-mcp)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M139](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M134](/packages/roots-acorn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
