sylius/api-bundle
Sylius API Bundle integrates API Platform with Sylius to expose e-commerce resources through a REST/JSON-LD API. It provides resource configuration, serialization, filtering, pagination, and OpenAPI documentation for building custom storefront or admin clients.
Read this before you run composer require. sylius/api-bundle is a Symfony bundle, not a Laravel package. It ships as a read-only subtree split of src/Sylius/Bundle/ApiBundle out of the Sylius/Sylius monorepo, and it expects a Symfony HTTP kernel, the Sylius DI container, Doctrine ORM, and API Platform. There is no service provider, no facade, and no path to installing it into a Laravel app. The "5 stars / 0 dependents" numbers are a false signal from the split — real consumption flows transitively through sylius/sylius.
So the realistic starting point for a Laravel developer is: you do not install this package; you consume the API it defines. Practically, a Sylius instance (or Sylius Cloud) is running somewhere as a headless commerce service, and your Laravel app is the storefront/BFF/OMS calling its REST surface.
Before writing any code, resolve three questions:
sylius/api-bundle 1.12.x on API Platform 2.7; Sylius 2.x ships the API Platform 3/4 generation. The auth model (Lexik JWT vs. newer OAuth2), serialization groups, and pagination semantics are materially different. Ask the Sylius team to confirm before you build a client./api/v2/docs (JSON-LD/Hydra + OpenAPI). Download that JSON, commit it, and treat it as the contract you version against — not the Composer constraint. Your Laravel connector is tested against the snapshot, not against a Sylius semver.Minimum viable first step on the Laravel side:
composer require saloonphp/saloon spatie/laravel-data
php artisan make:saloon-connector SyliusConnector
Point the connector at https://shop.example.com/api/v2, add a bearer token authenticator, and hit GET /shop/products from tinker to confirm the round-trip. That's your day-one "hello world." The rest of the assessment is about productionizing that call.
Where to look first in the package source: src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ (the resource definitions and serialization groups that define the contract), Routing/ (how /api/v2/... is generated), and Serializer/ (the custom normalizers). Reading these is how you discover why a field looks the way it does — the OpenAPI spec tells you it's there, the bundle source tells you why.
Pattern 1 — Typed HTTP client, not a raw Guzzle wrapper. Every Sylius resource gets a Saloon connector method and a spatie/laravel-data DTO. This is the difference between a system you can refactor safely and one where every response shape is a stringly-typed array.
class SyliusConnector extends Connector
{
public function resolveBaseUrl(): string
{
return config('services.sylius.base_url'); // e.g. https://shop.example.com/api/v2
}
protected function defaultAuth(): Authenticator
{
return new BearerAuthenticator(app(SyliusTokenService::class)->current());
}
}
class GetProductRequest extends Request
{
public function __construct(private string $code) {}
public function resolveEndpoint(): string
{
return "/shop/products/{$this->code}";
}
}
final class ProductData extends Data
{
public function __construct(
public string $code,
public string $name,
public array $translations,
public PriceData $price,
public bool $enabled,
) {}
}
Pattern 2 — Cache decorator per resource. Catalog and price data change infrequently; don't hit Sylius on every page render. Wrap the connector in a CachedSyliusClient that keys on (resource, id, locale, channel) and uses Cache::remember with tag-based invalidation driven by Sylius webhooks. This is also your stale-cache fallback when Sylius is slow or down — the circuit breaker trips, and reads serve from cache rather than failing.
Pattern 3 — Token acquisition as a dedicated service, never in controllers. Lexik JWT on the Sylius side means you have to acquire and refresh tokens. That service should:
Cache::store('redis') with a lease slightly shorter than the JWT ttl.Lock so a thundering herd under load doesn't trip Sylius' login throttle — the sporadic-401s-under-load failure mode is almost always this.class SyliusTokenService
{
public function current(): string
{
return Cache::remember('sylius.jwt', now()->addMinutes(50), function () {
return Http::asForm()->post(config('services.sylius.token_url'), [
'grant_type' => 'password',
'username' => config('services.sylius.username'),
'password' => config('services.sylius.password'),
'client_id' => config('services.sylius.client_id'),
'client_secret' => config('services.sylius.client_secret'),
])->throw()->json('access_token');
});
}
}
Pattern 4 — Hydra pagination is not LengthAwarePaginator. API Platform returns hydra:member + hydra:view with hydra:next as a URL, not ?page=2. Normalize this in the connector so your controllers get a LengthAwarePaginator-shaped result. Filtering uses ?code[in][]=...-style query params generated by API Platform search filters — discover them from the OpenAPI spec, not by guessing.
Pattern 5 — Webhook ingestion as the write-path backlink. Sylius can fire webhooks on order state changes, inventory updates, and customer creation. Ingest these into a queue (php artisan queue:work) with an idempotency key derived from the event ID, and process them with a ->onQueue('sylius-events') job. This is the only durable way to keep your Laravel-side cache and any mirrored data consistent without polling.
Pattern 6 — Dual-run migration. Don't flip catalog reads from your existing source to Sylius in one release. Run both paths behind a feature flag, sample responses with a shadow-traffic middleware, and diff. Cut over domain-by-domain: catalog → pricing → stock → cart → checkout → orders. Checkout last, always.
Pattern 7 — Contract tests on the OpenAPI snapshot. Commit openapi.sylius.json next to your code, generate DTOs from it (or validate hand-written ones against it in CI), and fail the build when the upstream spec drifts. This is the guardrail that turns the "contract drift across upgrades" risk into a CI red build instead of a production incident.
sylius/api-bundle in your Laravel composer.json. It will resolve, but nothing will autoload because there's no Symfony kernel. The dependency you want is zero — you want an HTTP client and a committed spec.Sylius/Sylius upstream or forking the monorepo. Plan the escalation path before you need it.price go?", check Resources/config/api_resources/Product.xml in the Sylius source for the group attached to that property, then check which groups your request's Accept header / route triggers. The /shop/products and /admin/products endpoints expose different fields from the same entity.itemsPerPage=30), never trust a client to send a limit, and cache by page+channel+locale.variant field is "/api/v2/shop/product-variants/123", not 123. Either resolve via a second request (slow) or extract the trailing segment (fragile across Sylius versions — pin it in tests). Prefer code fields where the API exposes them.SyliusAuthException vs. SyliusClientException vs. SyliusServerException) — the runbook depends on distinguishing them.cart → addressed → shipping_selected → payment_selected → completed). If you cache a cart ID and the Sylius side expires or transitions it, you'll see 409 Conflict and 422 Unprocessable. Design the cart flow as a single-owner-per-request server-side operation with no client-side state, and treat the Sylius cart as the source of truth — the Laravel side only holds a pointer.If-Match / ETag where exposed) or accept a reconciliation job. Don't try to enforce strong consistency — you can't, and pretending you can is where the long-term defects live.DataProvider/DataPersister on the Sylius side. That's a Symfony job, and it belongs to whoever owns the Sylius deployment — not to the Laravel team. If the Laravel team is being asked to extend the API, that's a scoping signal to escalate.laravel-fuse or a hand-rolled CircuitBreaker in the connector), and a runbook. This is your entire customization surface. It's small, which is the point.docs/sylius.md, and re-verify the OpenAPI snapshot diff on every Sylius bump. The Composer constraint on your Laravel side resolves nothing about the remote service — treat the remote version as a declared runtime dependency, not a Composer one.How can I help you explore Laravel packages today?