deployteam/intercall
deployteam/intercall is a lightweight Laravel package for handling inter-service calls with a clean, fluent API. Configure endpoints, attach middleware, and keep communication between your applications consistent, predictable, and easy to test.
This is a framework-agnostic package, so treat it as a raw library rather than a Laravel package: there is no service provider, no facade, and no config publish step you can rely on. Budget 30–60 minutes for a source read before writing any application code.
Minimal steps to first message:
composer require deployteam/intercall and immediately pin it ("deployteam/intercall": "x.y.z" exact) — see the vendor-risk note below.composer.json (PHP constraint, PSR-4 root namespace), src/ top level for the contract interfaces (typically an event/message value object, a dispatcher/publisher, and a transport/driver interface), then the README example and tests/.AppServiceProvider::register() (or a small IntercallServiceProvider in app/Providers) and bind the transport interface to a concrete driver chosen by env:$this->app->singleton(Transport::class, fn ($app) => match (config('intercall.transport')) {
'redis' => new RedisTransport(config('intercall.redis')),
default => new InMemoryTransport(), // dev + tests
});
config/intercall.php with the transport DSN, a default source/system name, and an event-name map. Only read it via config(), never env(), so config:cache stays safe.First use case worth wiring up: one outbound event from your Laravel app to one other system — e.g. publish order.placed from your checkout flow so a billing service can react. Keep it one-way and fire-and-forget for the first iteration; add consumption once you trust the delivery semantics.
Publish from a queued, after-commit listener. The package is transport-agnostic and knows nothing about your database transaction. Never publish inline inside a DB transaction — the event can escape before commit (or survive a rollback). Bridge from Laravel's own events instead:
class PublishOrderPlaced implements ShouldQueue, ShouldHandleEventsAfterCommit
{
public function handle(OrderPlaced $event): void
{
$this->intercall->dispatch(new SystemEvent(
name: 'order.placed.v1',
payload: ['order_id' => $event->order->id, 'total' => $event->order->total_cents],
meta: ['correlation_id' => $event->correlationId],
));
}
}
Keep the package behind your own thin port. Define App\Contracts\EventBus with publish(string $name, array $payload, array $meta = []) and implement it with intercall. Every call site depends on your interface, not on the package. This is the single highest-value pattern here: it makes the dependency replaceable if the package stalls and gives you one place to add logging, retry, and correlation IDs.
Payloads as flat arrays/DTOs, versioned names. Serialize to scalars only: IDs, ISO-8601 strings, integer minor units. Do not put Eloquent models, Carbon instances, enums (unless you control the PHP version on both ends), or lazy-loading relations into the envelope. Encode the schema version in the event name (order.placed.v1) and treat a breaking payload change as a new name rather than an in-place edit.
Consumer as a long-running Artisan command. Since there is no queue integration, you own the loop:
class ConsumeIntercall extends Command
{
protected $signature = 'intercall:consume {--once}';
public function handle(Listener $listener): int
{
$listener->subscribe($this->handlers()); // name => callable/class
$this->option('once') ? $listener->tick() : $listener->run();
return self::SUCCESS;
}
}
Run under Supervisor with --once in a cron loop if you'd rather not manage long-lived processes. Prefer --once + cron for low volume; use the blocking loop for high volume. Either way, dispatch each inbound message to a Laravel job so you inherit retries, backoff, and failed_jobs rather than reimplementing them.
Map inbound events to Laravel events, not to logic. A single EventReceived job that resolves a handler class from a config map keeps routing declarative:
'handlers' => [
'invoice.paid.v1' => \App\Intercall\Handlers\MarkOrderPaid::class,
],
Test with the in-memory transport. Bind a fake in TestCase::setUp and assert on the recorded envelopes — publish assertions are far cheaper than standing up a broker in CI. For integration coverage, run the real transport in a single dedicated test suite, not the whole suite.
Transport swap by environment. sync/in-memory locally, the real broker in staging/prod. If the package's local transport records rather than delivers, add one docker-compose service for the broker so staging exercises real serialization.
composer show deployteam/intercall and git tag before trusting the version constraint. Pin exact, commit composer.lock, and keep the anti-corruption layer described above. Have a fallback plan (Symfony Messenger, Laravel's queue + a broker, or plain HTTP webhooks).ext-json, possibly ext-redis/ext-sockets) against your runtime — a mismatch surfaces at runtime, not install time.SIGTERM handling so in-flight messages aren't dropped), dead-letter handling, retry counts, backoff, and observability. Assume at-least-once delivery until the source proves otherwise; make every handler idempotent with a dedupe key (event ID or a natural key like order_id + name + version).memory_get_peak_usage per N messages), and be aware Laravel keeps DB connections and model state alive across iterations. DB::purge() periodically or use --once cron mode. Under Octane, never resolve the listener as a singleton if it caches per-request state.{"total": "19.99"} and a consumer expecting 1999 fails silently-ish and late. Write one test that asserts the exact JSON envelope shape, and validate inbound payloads against a schema in the handler before touching your domain.config/intercall.php yourself — keep it env-free inside the file, and rebuild caches (php artisan config:cache) in deploy since the package may read config only at construction time.How can I help you explore Laravel packages today?