jeffersongoncalves/laravel-shopify
A Laravel package for integrating Shopify stores into your application, with helpers for OAuth authentication, API requests, and webhook handling so you can build and manage commerce features more easily.
Architecture fit. The package is a thin, resource-oriented client over the Shopify Admin API, exposed through a Laravel facade (Shopify::) with pre-built resource namespaces (shop(), products(), orders(), customers(), webhooks()) plus two escape hatches: Shopify::graphql() and raw Shopify::get/post/put/delete(). This is the right shape for a Laravel codebase — it slots in as a service-provider/facade dependency and keeps Shopify's HTTP idiom out of our domain layer. The deliberate coverage of the "cheapest way to verify a token" (shop()) is a small but telling design signal: the maintainer is thinking about install/verification flows, not just CRUD.
The practical ceiling is worth stating plainly. This is a transport and ergonomics layer, not a domain layer. There is no caching, no local model/entity mapping, no sync engine, no OAuth install controller, no webhook HMAC verification or receiver, and no documented pagination or rate-limit strategy. For a store with meaningful order volume we would still own: token storage and refresh, API version pinning, pagination cursors, retry/backoff, webhook intake and verification, and idempotency on writes.
Integration feasibility. High for a Laravel monolith or a dedicated "commerce integration" service. Composer install, publish config, supply a shop domain and access token, and shop() returns a record — that is a sub-day spike, not a project. The domain normalization (handle, *.myshopify.com, or a pasted admin URL) removes a class of support tickets and configuration bugs that typically show up in multi-tenant setups where a merchant pastes whatever is in their address bar.
Two API-surface details materially reduce risk:
errors inside HTTP 200 converted to an exception is the single most valuable line in these release notes. Shopify's Admin GraphQL returns 200 with a top-level errors array on failure, and silent-success bugs from that are a classic production incident. Catching it at the client boundary is correct.ShopifyException on every non-2xx, carrying message and status code gives us a single catch point and enough structure to map 401 → re-auth/reinstall, 404 → resource gone, 429 → backoff, 5xx → retry. That's a clean error taxonomy to build on.Technical risk. This is a 1.0.0 first release with no prior assessment and no usage history in our estate. Treat it as unproven until a spike says otherwise.
| Risk | Severity | Mitigation |
|---|---|---|
Rate limiting not addressed. Shopify enforces a leaky bucket (REST ~2 req/s sustained; GraphQL cost-based). No mention of throttling, 429 handling, or Retry-After respect. |
High | Wrap calls in our own limiter; verify 429 behavior in spike; budget GraphQL cost explicitly |
Pagination undocumented. List endpoints (products, orders, customers) exist, but cursor semantics and whether the client auto-pages are unstated. A list() that silently returns only page one is a correctness bug in reports and syncs. |
High | Pin behavior with tests before any bulk use |
| No webhook HMAC verification or receiver. We can register webhooks but not safely consume them. | Medium | Build intake ourselves; do not assume the package covers it |
| No OAuth install/token lifecycle. Assumes a token exists. Multi-tenant onboarding is ours. | Medium | Own an install flow; store tokens encrypted |
| No API version pinning visible. Shopify deprecates Admin API versions quarterly. | Medium | Explicitly set and document the version; calendar the upgrade |
| Bus factor / maintenance unknown. Single first release, no stated support model. | Medium | Vendor-risk review; keep the escape hatch as our exit ramp |
Escape hatches can become the norm. Shopify::get() and graphql() make it easy to bypass the resource layer, scattering Shopify specifics across the codebase. |
Low–Medium | Convention: escape hatch only in the integration module, never in controllers |
Key questions for the maintainer / spike:
list() auto-paginate, expose cursors, or return a single page? What is the return type?ShopifyException with status 429, or is there built-in retry/backoff? Is Retry-After honored?graphql() expose cost/throttle metadata (extensions.cost) so we can self-throttle?Http::fake() sufficient, or is there a first-class fake/mock client?bulkOperationRunQuery) for large exports, or does that go through the escape hatch?userErrors mutation response — is that also surfaced, or only top-level errors?Stack fit. Laravel-native: facade, config-driven credentials, exceptions that extend a base ShopifyException. Fits our existing HTTP client conventions and requires no new infrastructure — no queue, no cache, no database. That makes it viable in both a monolith and a small integration service. The relevant question is not "does it fit Laravel" but "does it fit our integration boundary": it should live behind our own repository/gateway interfaces so that Shopify types and exception semantics do not leak into domain code.
Migration path. Greenfield-friendly. There is no prior assessment and no incumbent client named, so this is an adoption decision rather than a migration. If we already have ad-hoc Http:: calls to Shopify scattered around, the migration is incremental and low-risk: introduce the package as a new dependency, route new work through it, then opportunistically replace existing call sites module by module. The graphql() and raw-method escape hatches mean we never hit a hard blocker mid-migration — uncovered endpoints have a defined fallback. That is the right deprecation-free path for a 1.0.0.
Compatibility. Verify against our PHP and Laravel version matrix before committing; a first release is unlikely to be tested across our full supported range. Two compatibility surfaces matter more than the framework version:
Sequencing.
shop(), a paginated products() list, one orders() list, one mutation, one GraphQL query, and one deliberate failure to confirm exception shape. Close out the key questions above.ShopifyGateway interface; implement it with this package. All escape-hatch usage confined to that namespace.shop()-based verification, 401 → reinstall handling. Nothing else is safe until this exists.cancel or fulfillment is a merchant-facing incident.Maintenance. Low surface area to patch — it's a client library, not a runtime dependency with its own state, so upgrades are routine. The real maintenance load is external: Shopify's quarterly Admin API versioning and deprecations. Budget a recurring review each release cycle; deprecations arrive on a schedule whether or not the package is ready. Our wrapper layer is what makes that a contained task instead of a codebase-wide sweep. Also track the package's own release cadence — a 1.0.0 with no history gives us no signal yet on responsiveness to API changes.
Support. Internal support burden concentrates in three places: (1) bad credentials → 401, which should map to a clear "reconnect your store" UX rather than a stack trace; (2) domain misconfiguration, largely defused by the handle/myshopify/admin-URL normalization; (3) rate-limit errors, which will surface as merchant-visible slowness or failed actions if we don't throttle proactively. Because errors carry message and status code, we can map to actionable user messaging rather than generic failures — do that in the wrapper, once.
Scaling. The package is stateless and cheap; scaling is bounded by Shopify's limits, not ours. Two numbers govern capacity: REST's leaky bucket (~2 req/s sustained, burst to 40) and GraphQL's calculated-cost budget. Consequences:
Failure modes. Ranked by likelihood × impact:
| Failure | Detection | Response |
|---|---|---|
| 429 / throttled — most likely under load | ShopifyException status 429 |
Bounded exponential backoff + queue; alert on sustained rate |
| 401 token revoked / app uninstalled | status 401 | Mark store disconnected, halt jobs, prompt reinstall; don't retry |
| Partial pagination — silent truncated results | Diff counts vs. Shopify admin | Contract-test pagination in spike; treat unknown as blocking |
GraphQL userErrors in 200 mutation |
Unclear if covered | Validate: top-level errors is handled, userErrors may not be. Wrap mutations ourselves |
| Silent webhook loss — no HMAC/retry design | Missing events downstream | Our intake must be idempotent with a reconciliation job |
| API version deprecation | Warnings, then hard removal | Pinned version + scheduled upgrade review |
| Uncovered endpoint | Feature gap | Escape hatch — acceptable, but only inside the wrapper |
Ramp-up. Very low for a Laravel developer: the facade reads almost like documentation, and shop() as a token check is an obvious first thing to try. Expect a competent developer productive in half a day on read paths. The learning curve is not the package — it's Shopify's operational semantics (leaky bucket, cost-based GraphQL, webhook delivery guarantees, API versioning). Don't confuse the two when estimating: the spike should be framed as "learn Shopify's limits," not "learn this library."
TPM recommendation. Adopt for a bounded pilot — read paths plus token verification in one feature — with the wrapper interface in place from day one. Do not commit write paths (cancel, close, fulfillment) to production until rate limiting, pagination semantics, and mutation userErrors handling are verified by test. The package earns its place on GraphQL error handling and error taxonomy alone; the gaps are all things we would have written anyway, and the escape hatches mean adopting it doesn't block us.
How can I help you explore Laravel packages today?