complex-heart/on-laravel
complex-heart/on-laravel is a Laravel integration package from Complex Heart. It wires the On package into Laravel with service provider registration, configuration publishing, and framework-friendly conventions, making it easier to use On components inside Laravel applications.
Install and confirm the baseline first. The package is a thin Laravel wrapper around the ComplexHeart SDK, so treat it as two dependencies in one: the wrapper (service provider, config, facade/binding) and the underlying SDK (HTTP client, auth, DTOs). Run composer require complex-heart/on-laravel and check composer show complex-heart/on-laravel for the resolved SDK version — with 2 stars and zero dependents, the version you land on is effectively the version you own.
Where to look, in order:
composer.json of the package — require block tells you which SDK version and which Laravel/PHP range is actually supported. The wrapper is usually more permissive than the SDK.src/ for the service provider — confirms whether the client is registered as a singleton, a facade accessor, or bound to an interface you can swap.config/ (or config/*.php in the package root) — the authoritative list of settings and the exact env var names. Do not infer env keys from the README; read the config file's env() calls.tests/ — for a low-adoption package this is the best documentation of intended usage and the fastest way to see how the client is faked.vendor/complex-heart/ — this is where the real API surface, exception classes, and retry behaviour live.Bootstrap steps:
php artisan vendor:publish --provider="ComplexHeart\OnLaravel\...ServiceProvider" --tag=config (confirm the tag from the provider; many packages ship only the untagged default).bootstrap/providers.php (Laravel 11+) or config/app.php..env, not in the published config file, so config:cache and per-environment deploys stay clean.config/services.php or your .env early — you want a non-production target before you write any feature code.First use case: don't start with the interesting domain logic. Write one Artisan command or one route that performs a single round-trip against the sandbox (php artisan tinker is fine for a first probe) and dumps the raw response. This flushes out auth, base URL, TLS/proxy, and timeout issues in isolation, before they are tangled up with your application code. Once that call is green, wrap it in a small adapter class (see below) and never call the SDK directly from controllers again.
Wrap the SDK behind your own contract. This is the single highest-value pattern for a package with this adoption profile. Define App\Contracts\ComplexHeartClient with the three or four methods you actually use, implement it with an adapter that delegates to the package's client, and bind it in a RepositoryServiceProvider. Benefits: your domain code is testable without the SDK, you can swap the package out in an afternoon, and SDK type changes stop at one file.
$this->app->singleton(ComplexHeartClient::class, fn ($app) => new SdkComplexHeartClient(
$app->make(\ComplexHeart\OnLaravel\Facade::class) // or the resolved client binding
));
Prefer constructor injection over the facade. The facade is convenient in a REPL, but injection keeps the dependency visible and lets you bind a fake in tests without touching Facade::shouldReceive. If the package only exposes a facade, resolve $app->make(...) inside your adapter and expose the interface outward.
Configuration as a typed object. Rather than sprinkling config('complex-heart.…') through the codebase, build a small value object in a service provider (ComplexHeartConfig::fromArray(config('complex-heart'))). It gives you one place to validate required keys, coerces booleans/ints correctly (env strings like "false" are truthy), and fails fast at boot instead of mid-request.
Push outbound calls onto the queue. Any call to an external SDK is a latency and availability risk. Dispatch a job, let Laravel handle retries with backoff, and keep the request cycle clean. Give the job $tries, backoff(), and a failed() handler that logs with the correlation ID you attached. Also set an explicit retryUntil — SDK-default retries plus queue retries multiply quickly.
Multi-tenant / per-user context. If the SDK is scoped to an account or token per tenant, don't share a single singleton across tenants in a long-lived worker. Two workable patterns: (a) resolve a per-tenant client from a factory keyed by tenant ID, cached in a request-scoped container binding; (b) bind the singleton but call a ->withContext($tenant) method before each use if the SDK supports it. Choose (a) unless the SDK is explicitly stateless.
Testing. In TestCase or per-test, bind a stub implementation of your contract — this is the reason the adapter exists. If you must test through the package's facade, use Facade::shouldReceive(...) and call Facade::clearResolvedInstances() in tearDown. For an integration test against the real sandbox, mark it with @group external and exclude it from the default suite so CI stays deterministic.
Domain decoupling via events. Let the SDK listener be a passive consumer: fire OrderPlaced, and have a queued listener that performs the ComplexHeart call. This keeps your write path fast and means an SDK outage degrades a feature rather than failing a checkout.
env() outside config files returns null under config:cache. The classic Laravel trap, and it bites hard in packages that read env vars in a service provider or constructor. If credentials "work locally, break in production," this is almost always it. Publish the config and reference config('complex-heart.*') everywhere.
Pin the version and pin the SDK with it. With zero dependents and a very recent release date, you are a de facto beta tester. Use an exact constraint ("complex-heart/on-laravel": "1.2.3") or at minimum a tight tilde, commit composer.lock, and read the CHANGELOG before every bump. Budget an hour to read src/ — it is likely small, and knowing it removes most of the uncertainty.
Map SDK exceptions to your own. SDK exception classes leaking into controllers means a vendor outage becomes a 500 with a stack trace containing a token. Catch the SDK's base exception in your adapter, log the correlation/request ID, and rethrow a domain-level IntegrationUnavailable that your handler renders as 503. Never let \Throwable from a network client reach the default handler.
Set timeouts explicitly. Guzzle's default is no timeout, which in practice means a hung worker or a PHP-FPM pool exhaustion event. Configure connect and total timeouts in the published config (or in the SDK client options the wrapper forwards) and make them shorter than your queue's retry_after.
Octane and long-lived workers. Singletons holding per-request state leak between requests under Octane, and stale auth tokens are the usual symptom. If you run Octane, add a RequestReceived/RequestTerminated listener to flush the resolved client, or register the binding as scoped rather than singleton.
Redact secrets and PII in logs. The SDK will log request bodies at debug level if given a logger. Register a Monolog processor that scrubs the API key, bearer tokens, and any user identifiers before they hit disk, and never dd() a raw response in a shared environment.
Watch for facade static caching in tests. A facade that caches the resolved instance survives RefreshDatabase and produces order-dependent test failures. Clear it in tearDown.
Extension points worth knowing: the published config is the supported override surface; the service provider's bindings are the practical one (rebind the client, bind a custom HTTP client, or decorate the adapter). If the SDK accepts a PSR-18/Guzzle client, inject one with your app's middleware — that gives you tracing, retries, and log correlation for free without patching the package.
Fallback plan. Keep the adapter's method signatures aligned with the SDK's semantics but not its types. If the package goes unmaintained, replacing it with ~100 lines of direct HTTP calls behind the same interface is a contained, testable change rather than a refactor.
How can I help you explore Laravel packages today?