splash/recommerce
splash/recommerce is a Laravel-native foundation for building recommerce and e-commerce flows, with models and services for products, carts, checkout, orders, and returns. It plugs into existing Laravel apps and keeps storefront business logic testable and configurable.
Install
composer require splash/recommerce
The package is an OpenAPI-generated client, so there is no migrations, no publishable config file, and typically no service provider bindings beyond what splash/connector-utils provides. You wire it up yourself.
Where to look first
vendor/splash/recommerce/src/Client.php — the generated client. Every endpoint on the ReCommerce API surfaces here as a method (getOrders(), createReturn(), getProduct() …). Skim it; it is the API documentation.vendor/splash/recommerce/src/Model/ — generated DTOs for requests and responses. Property names here map 1:1 to the JSON payloads.composer.json — check the required PHP version and whether it pins psr/http-client / jane-php/* versions you already have.First call in a Laravel app
use Splash\ReCommerce\Client;
use Splash\ConnectorUtils\Connector;
// In a service provider or a dedicated factory
$this->app->singleton(Client::class, function () {
$client = new Client();
$client->setBaseUrl(config('services.recommerce.base_url'));
// Most Splash connectors accept either a bearer token or API key
$client->setApiKey(config('services.recommerce.api_key'));
return $client;
});
public function index(Client $client)
{
$orders = $client->getOrders(['status' => 'open']);
return view('orders.index', ['orders' => $orders]);
}
Configure services.recommerce.* in config/services.php and put values in .env. Then make one read-only endpoint call in a Tinker session to confirm the base URL and auth header format are correct before writing anything else.
Wrap the client, don't inject it everywhere
The generated client leaks transport concerns and unstable generated model types through your codebase. Put a thin adapter around it:
final class ReCommerceGateway
{
public function __construct(private Client $client) {}
public function openOrders(): array
{
return $this->client->getOrders(['status' => 'open'])->getData();
}
}
Bind ReCommerceGateway in the container, and mock the gateway in tests instead of the generated client.
Use a single configured instance
ReCommerce's generated clients are stateful — setBaseUrl, setApiKey, and the PSR-18 client all live on the instance. Register it as a singleton in a service provider so you're not re-running config on every request, and so retries/timeouts on the underlying HTTP client apply consistently.
Swap in the HTTP client for testing and observability
Splash connectors are built on psr/http-client (via HTTPlug/jane-php). That means you can pass a MockHttpClient in tests or a client decorated with logging middleware (e.g. Http\Client\Common\Plugin\LoggerPlugin) without touching package code:
$client = new Client();
$client->setHttpClient(new MockHttpClient([$response]));
This is the cleanest way to write tests against the connector — assert the request your code produced, not just the response you consumed.
Paginate by hand
Generated clients rarely expose a paginate() iterator. Look for page / perPage / offset parameters on the list methods and write a simple generator in your gateway:
public function allOrders(): \Generator
{
$page = 1;
do {
$batch = $this->client->getOrders(['page' => $page, 'per_page' => 100])->getData();
yield from $batch;
$page++;
} while (count($batch) === 100);
}
Integrate via queued jobs, not request cycle
ReCommerce calls are e-commerce-adjacent — order lookups, return creation, product fetches. Anything write-shaped (creating returns, updating fulfilment state) should live in a queued job with retry semantics. Read-shaped calls in the request path are fine for low-traffic admin screens, but cache product/lookup data with Cache::remember — these are slow, rate-limited external calls.
Treat responses as DTOs
Generated response models (Model\Order, Model\LineItem) are plain objects with getters. Convert them into your own domain models at the gateway boundary:
public function mapOrder(Model\Order $order): Order
{
return new Order(
id: $order->getId(),
status: $order->getStatus(),
total: $order->getTotal()->getAmount(),
);
}
This means a splash/recommerce upgrade that renames a generated property doesn't ripple through your controllers.
Generated code has no semantic versioning safety
Because the client is generated from an OpenAPI spec, minor releases can break you silently — an endpoint may be regenerated with a different method signature or a model property may be renamed. Pin the version in composer.json with ~ and read the diff before bumping. There is no README guarantee that method names are stable.
Exception types are jane-php's, not Laravel's
Expect Http\Client\Exception\ClientException for 4xx and ServerException for 5xx. They do not extend Illuminate\... classes, so $e instanceof ModelNotFoundException checks will not fire. Centralise handling in a gateway decorator:
try {
return $this->client->getOrder($id);
} catch (ClientException $e) {
if ($e->getResponse()?->getStatusCode() === 404) {
throw new OrderNotFound($id);
}
throw $e;
}
Auth header format is not obvious
Depending on which Splash connector variant this is derived from, auth is either Authorization: Bearer <token> or an X-Api-Key header. The setApiKey() method name suggests the latter, but verify against the spec — sending the wrong header usually produces a generic 401 and wastes an hour.
Base URL matters for sandbox vs. production
Splash connectors target an embedded/proxy environment, not api.recommerce.com directly. Confirm the base URL with the repo maintainers rather than guessing; the default in Client.php may point at a staging host.
No Laravel facade or auto-registration
Unlike first-party packages, this won't appear in config/app.php automatically and there's no ReCommerce:: facade. If you want that ergonomics, write your own service provider and facade around the singleton.
Rate limiting is your responsibility
Generated clients don't rate-limit. If you're syncing orders or products on a schedule, add a RateLimiter::for('recommerce', ...) in your job and throttle with ->middleware(['throttle:recommerce']). Hitting the provider's limit tends to surface as 429s that jane-php throws as generic ClientException.
Testing against generated models is brittle
Because the generated models often lack constructors with defaults, unit-testing a mapping function means constructing a Model\Order via setters rather than new Order(...). Prefer a fixture JSON file plus a real deserialisation via the generated serializer (jane-php's Serializer\ArrayTransformer) over hand-building models.
Extension points are limited
There's not much to override — extend the Client subclass if you need to inject default headers (e.g. an X-Request-Id for tracing), and use PSR-18 middleware for anything cross-cutting. There is no event/hook system.
Zero dependents, low score — treat as internal
With no dependents and no stars, this is effectively a privately consumed package. Expect to read the source to answer most questions, and be prepared to fork if you need a fix. Log a composer.json constraint and consider vendoring via a fork for stability.
How can I help you explore Laravel packages today?