vin-sw/shopware-sdk
A PHP SDK for Shopware 6 APIs, offering object-oriented access to the Admin API (CRUD, Sync, User Config, Notification, Search) plus webhook registration, authentication, and receiver helpers. Compatible with Shopware 6.5+ on SDK 2.x.
Install and pin the right major version first. The SDK major version tracks the Shopware schema, not the SDK's own feature set:
composer require vin-sw/shopware-sdk
| SDK | Shopware |
|---|---|
| 2.x | >= 6.5 (2.2.0 adds 6.6.10 compatibility) |
| 1.x | 6.4 |
Requirements are ext-curl and PHP >= 7.4. For a Laravel app on Shopware 6.5/6.6, use 2.x and pin with a caret constraint in composer.json — the DAL entity classes are generated from the Shopware schema, so a minor bump can change property names.
There is no Laravel service provider. You wire it yourself. Add a config/shopware.php with url, client_id, client_secret, and register a scoped binding in AppServiceProvider:
$this->app->scoped(Context::class, function () {
$grant = new ClientCredentialsGrantType(
config('shopware.client_id'),
config('shopware.client_secret'),
);
$token = (new AdminAuthenticator($grant, config('shopware.url')))->fetchAccessToken();
return new Context(config('shopware.url'), $token);
});
Where to look first: the examples/ folder is the real documentation — authentication.php, entity-repository.php, sync-service.php, info-service.php, custom-entity.php. Then src/Data/Entity (one Definition/Entity/Collection trio per Shopware entity) and src/Service. The README's screenshots are just these examples rendered.
First use case — read a product:
$repo = RepositoryFactory::create(ProductDefinition::ENTITY_NAME);
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('shippingFree', true));
$criteria->addAssociation('manufacturer');
$criteria->setLimit(50);
$products = $repo->search($criteria, $context);
If you already know Shopware's DAL from writing admin plugins, the API is deliberately identical — that's the whole value proposition. If you don't, budget an afternoon reading Shopware's DAL docs; the SDK mirrors them rather than inventing its own abstraction.
Token handling is your job, not the SDK's. fetchAccessToken() hits the network every call. Store the token per shop (cache or DB column with expires_in) and only re-auth on expiry or 401. This is the single biggest performance and reliability decision in the integration.
Multi-tenant: one Context per shop. In a Laravel app connecting to several Shopware instances, resolve the context from a Shop model rather than a global singleton:
final class ShopwareContextFactory
{
public function forShop(Shop $shop): Context
{
$token = Cache::remember("sw:token:{$shop->id}", 540, fn () => (new AdminAuthenticator(
new ClientCredentialsGrantType($shop->client_id, $shop->client_secret),
$shop->url,
))->fetchAccessToken());
return new Context($shop->url, $token);
}
}
Repositories cover the whole CRUD surface: get, search, searchIds, create, update, delete, syncDeleted, createVersion, mergeVersion, deleteVersion, clone, schema, plus aggregate (2.2.0). searchIds is the cheap path when you only need IDs — pair it with a batched get.
Use partial loading on wide entities. Products and orders have huge payloads; Criteria::addFields() (2.2.0) plus explicit addAssociation() keeps responses small and avoids the memory blowups that show up in the issue tracker on large searches.
Bulk work goes through SyncService, not loops of create. Build a SyncPayload per entity and send in batches. Since 2.2.0 the payload accepts criteria, and syncDeleted uses the same HTTP client as the repository, so a custom client or middleware applies uniformly.
Services are the day-to-day shortcuts — each takes Context as its first argument:
InfoService — cheapest possible health check; wire it into a scheduled Artisan command to detect dead shops before a queue fills with failures.StateMachineService — order state transitions without hand-rolling /state-machine/state/transition.MediaService — product image uploads.NotificationService — push admin toasts after an external job finishes.AdminSearchService — one request across multiple entities, ideal for a global admin search box.AdminActionService — the escape hatch. Any endpoint without a concrete class goes through here; wrap it in your own typed client so call sites stay readable.Webhooks slot into two Laravel routes. Registration returns an object you persist; confirmation verifies the secret against the raw request body:
public function confirm(Request $request, ShopRepository $shops): Response
{
$shop = $shops->findByShopId($request->input('shopId'));
if (! WebhookAuthenticator::authenticatePostRequest($shop->secret)) {
return response(null, 401);
}
$shops->updateAccessKeys($shop->id, $request->input('apiKey'), $request->input('secretKey'));
return response()->noContent();
}
Action-button endpoints return PSR-7 responses (EmptyResponse, ReloadDataResponse, NotificationResponse, OpenModalResponse). Bridge them to Laravel with symfony/psr-http-message-bridge, or read the status/headers off the object and build a response() manually — the latter is less machinery for four classes.
Queue everything that writes. Sync payloads, media uploads, and notification pushes are network-bound and belong in jobs with retries and backoff, so a Shopware hiccup doesn't take down a user request.
Criteria::getPage() returns null when unset. This was a bug fix in 2.4.0, so code written against 2.3 may assume an int. Type-hint accordingly.search() materialises a collection. For exports, page with setPage()/setLimit() and process in chunks; the "Allowed memory size exhausted" issue fixed in 2.2.0 shows what happens otherwise.null usually means a missing addAssociation(), not missing data. Entities with two associations of the same type were broken until 2.2.0 — upgrade if you hit it.ShopUnreachableException exists (2.2.0) but nothing retries for you. Catch it at the job level and back off; a timeout on a 5-second default will happen in production.searchIds can throw ShopwareSearchResponseException (fixed in 1.7.2 for not throwing). Catch it and log the Shopware error payload — API-level errors are frequently field-level validation on filters, not outages.examples/custom-entity.php. A CustomDefinition not found error means the definition wasn't registered; fixed structurally in 2.2.0 but still requires your class.$request->all() after Laravel has parsed the JSON will not verify. Read $request->getContent() and don't add middleware that consumes the stream before your route.AdminAuthenticator constructors to inject logging or metrics; use RepositoryFactory with custom definitions for bespoke entities; use AdminActionService for endpoints the SDK hasn't covered yet (the README has an open TODO list).Shape-and-Shift/shopware-laravel-sdk) targets Laravel 8. Keep the SDK behind your own thin repository/service interfaces so a fork or raw-HTTP fallback is a contained change, not a rewrite.Data/Entity classes almost always trace back to a schema mismatch between your SDK version and the shop's Shopware version, not to bad data.How can I help you explore Laravel packages today?