splash/shopify
splash/shopify is a Laravel package for connecting your application to Shopify. It provides a convenient PHP API layer for common store operations, helping you integrate products, orders, and webhooks into familiar Laravel workflows with less boilerplate.
splash/shopify is not a general-purpose Shopify SDK — it is a connector plugin for the Splash Sync platform. It assumes you are running a Splash node (the splash/core + splash/connectors stack, usually via a Splash Manager or an existing Symfony app), and it teaches Splash how to read and write Shopify objects. If you just want to call the Shopify Admin API, use shopify/shopify-api instead.
Minimal path to a running connector:
composer require splash/shopify
config/bundles.php (or app/AppKernel.php on older Symfony):
Splash\Connectors\Shopify\ShopifyBundle::class => ['all' => true],
my-store.myshopify.com), an Admin API access token, and usually an API version. These are persisted as Splash Parameters, not as .env values.read_products/write_products, read_orders, read_customers for typical syncs.Where to look first, in this order:
README.md — short, but states the supported object list and any API-version caveats.src/Services/ — the connector's API wrapper (typically ShopifyService) and ConfigurationService. This is where the Guzzle client, auth headers, and pagination live. Read it before you touch anything else.src/Objects/ — one class per synced type (ThirdParty/customer, Product, Order, and often ProductVariant/Image as child objects). Each declares its fields and its fetch/write behaviour.Resources/config/services.yaml — shows which services are wired and how the connector is parameterised.With 2 stars and zero dependents, treat the source as the documentation. Budget an hour to read Services/ and one representative Objects/ class before writing code.
The connector is a mapper, not a workflow engine. Every object class implements Splash's object contract: it declares fields (via a FieldsFactory), reads a remote record into a Splash object, and applies local changes back to Shopify. Your day-to-day work is almost entirely inside that contract.
Adding a field to an existing object. The common task. Open the relevant class in src/Objects/, register the field in the field-definition method with the right type and flags (REQUIRED, READ_ONLY, NOT_TESTED, LIST for collections), then populate it in the fetch method and handle it in the write method. If the field maps to something the connector caches, check whether the cache object needs the property too.
Adding a whole new object type. Copy the closest existing class, then:
getAvailableObjects()-style method or a service declaration).Resources/config/services.yaml with the splash.object tag.getDescription(), getFields(), doRead(), doWrite(), and the delete hook if Shopify allows deletion for that type.Calling Shopify directly from your own code. Inject ShopifyService if you need an ad-hoc call (a one-off report, a bulk export, a webhook re-fetch). Keep it out of the object classes' hot paths — round trips inside doRead() are multiplied by however many objects Splash commits, and Shopify rate limits will bite quickly.
Push vs pull direction. Splash commits are directional per object type. In practice you end up with asymmetric configurations: products usually pushed from your PIM/ERP to Shopify, orders and customers usually pulled from Shopify. Configure this in the connector's object settings rather than in code; the connector rarely needs modification to flip direction.
Webhook-assisted sync. Shopify's webhooks (orders/create, products/update, customers/update) are the sane way to keep latency low. The connector itself is poll-based; add a small controller in your host app that verifies the HMAC header and then triggers a targeted Splash commit through the Splash client API. Keep the webhook handler thin — it should enqueue, not synchronously hit Splash.
Testing against a dev store. Create a free Shopify Partner development store, install your app there, and point the connector's parameters at it. Development stores do not charge for API usage and let you generate test orders via Bogus Gateway. Never develop against a live store; the connector will happily write bad data on the first misconfigured field.
Multistore / multilocale. If you have several Shopify stores, you run several Splash connectors, one per shop domain. Do not try to multiplex shops behind one connector instance — the parameters are bound per connector and the object caching assumes a single shop context.
API version drift is the number one maintenance cost. Shopify ships a new Admin API version quarterly and sunsets old ones after about 12 months. The connector pins a version string in its configuration; when that version retires, every request starts failing with a deprecation or 404. Watch the Shopify changelog and treat the version bump as a scheduled dependency upgrade, because Composer will not warn you.
Rate limits are per-store and leaky-bucket. REST is roughly 2 requests/second sustained (40-burst for standard plans); GraphQL is cost-based. A naive doRead() that fetches products then loops over variants then loops over images per variant will exhaust the bucket on any catalogue over a few hundred SKUs. Batch where the API allows, and honour the Retry-After / X-Shopify-Shop-Api-Call-Limit headers in the service layer rather than in each object.
Pagination is cursor/Link-header based, not page-number based. If you write custom fetch code, parse the Link header's rel="next" URL. Page-number pagination against the REST Admin API is unreliable on filtered collections and will silently skip records.
Nested objects are separate objects. Products do not carry variants and images inline in Splash — they are child objects that need their own field definitions and their own sync ordering. If you add a product field but forget the variant mapping, you will get products with no prices and spend an afternoon confused.
Access scopes fail at runtime, not at configuration. A missing read_customers scope gives you a 403 on the first customer fetch, long after you thought the connector was healthy. Validate the full scope set with a smoke-test commit against every enabled object type before going live.
Read-only Shopify fields. Order status, fulfilment state, and several financial fields are not writable via the Admin API the way a naive mapping suggests — they are driven by Shopify's own state machine and dedicated endpoints. Mark them READ_ONLY in the field definition rather than letting the write path fail silently.
Debugging. Splash routes connector logs through its own logger. Turn the connector's debug/verbose parameter on and watch the log for the raw request URL and response body; the object-level error messages are deliberately vague about HTTP detail. For anything the connector swallows, add temporary logging in ShopifyService around the response.
Deleted objects. Shopify does not always surface deletions — a deleted product simply stops appearing in list calls. If downstream systems need tombstones, handle it via the products/delete webhook and mark the object deleted in your own system; don't expect the connector's fetch loop to notice.
Extension points are limited and unversioned. The cleanest place to customise is a subclass of the object class registered under the same Splash tag, plus overriding the service alias. But with 2 stars, 0 dependents, and no meaningful issue history, assume the maintainers can rename internal classes between releases — pin the exact version in composer.json and read the diff before every bump.
How can I help you explore Laravel packages today?