Symfony Bundle · PHP 8.2+ · Packagist
Every API built differently is technical debt. IntegrationEngine gives your team one pattern — an Action, a Mapper, a DTO — repeated across every integration, no exceptions. The structure is not up for debate: it is already defined.
Sound familiar?
If you are on this page, you already know these. They are not new. The question is what you do with the next API you have to integrate.
No shared pattern means days of discovery and arbitrary decisions every time a new API lands on the backlog.
A different structure per integration means every new teammate has to start from zero, every time.
HTTP logic scattered across services, no contract to enforce, nothing to test in isolation.
Auth, caching, mapping — reinvented from scratch every single time, in every integration.
HTTP coupled to domain logic. You cannot test business rules without hitting the network.
Six months later, the developer who built it is gone and the code is unreadable.
What about your existing integrations?
IntegrationEngine lives alongside your existing code. Start with the next integration you add to the project. The old ones can keep running as they always have — migrating them becomes a planned technical decision, not an emergency.
How It Works
A single flow for all your external APIs. Every step has a clear, testable owner.
make:integration scaffolds Action, Mapper, Response DTO and YAML in one command. You write only business logic.
Every integration follows the same layout and the same contracts. Instant onboarding for every new teammate.
OAuth, sessions, API keys — fetched, cached, and auto-refreshed on 401. No manual token logic, ever.
Every endpoint returns a guaranteed DTO. No guessing at runtime, no silent surprises in production.
sendMany() runs N concurrent requests. Individual failures never abort the batch.
Concrete action, integration facade, or bundle contract — each layer is independently extensible. Swap HTTP client or cache backend with one line in YAML.
One command generates everything
One command generates all the scaffolding. Your team starts writing code that everyone else recognises.
$ php bin/console make:integration MyApi GetEmployee
The Call Site
The engine lives inside your integration facade. From the outside, your domain never knows HTTP exists.
final class MyApiIntegration { public function __construct( private IntegrationEngine $engine ) {} public function listEmployees(): GetEmployeesResponse { $response = $this->engine->send( GetEmployeesAction::getName() ); \assert($response instanceof GetEmployeesResponse); return $response; } }
final class MyApiIntegration { public function getEmployee(int $id): GetEmployeeResponse { $response = $this->engine->send( actionName: GetEmployeeAction::getName(), context: DefaultActionContext::create(['id' => $id]), ); \assert($response instanceof GetEmployeeResponse); return $response; } }
final class MyApiIntegration { public function createEmployee( string $name, string $correlationId, ): CreateEmployeeResponse { $response = $this->engine->send( actionName: CreateEmployeeAction::getName(), body: CreateEmployeeBody::create(['name' => $name]), headers: new CorrelationHeaders($correlationId), ); \assert($response instanceof CreateEmployeeResponse); return $response; } }
final class MyApiIntegration { public function getUser(int $id): GetUserResponse { $response = $this->engine->send( actionName: GetUserAction::getName(), body: GetUserBody::create(['id' => $id]), ); \assert($response instanceof GetUserResponse); return $response; } }
final class MyApiIntegration { public function getManyEmployees(array $ids): array { $requests = []; foreach ($ids as $id) { $requests[$id] = EngineRequest::create( GetEmployeeAction::getName(), DefaultActionContext::create(['id' => $id]), ); } $results = $this->engine->sendMany($requests); if ($results->hasFailures()) { throw array_values($results->errors())[0]; } return $results->responses(); } }
For the full pattern (facade → service → domain) → README →
Your existing integrations do not need to change. The next one can be different.
Already using it? Tell us how it goes → · Prefer to write directly? hi@integrationengine.dev