Symfony Bundle · PHP 8.2+ · Packagist
IntegrationEngine forces every integration to look the same. Ship faster. Onboard instantly. Stop doing archaeology forever.
Why not HttpClient?
Use HttpClient for one-off calls. Use IntegrationEngine when external APIs are part of your architecture — and you need your whole team to speak the same language six months from now.
The Problem
Without a shared standard, each developer solves it differently. The codebase fragments. Knowledge disappears. Every new API means starting from scratch.
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.
Six months later, the developer who built it is gone and the code is unreadable.
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.
HTTP client, cache backend, config source — each replaceable with one line in YAML.
One command generates everything
Answer three questions. Get a fully scaffolded, typed integration.
$ php bin/console make:integration MyApi GetEmployee
The Call Site
No HTTP clients. No request builders. No mappers. One clean facade method your entire team recognises.
// Action path: GET /employees $this->engine->send( GetEmployeesAction::getName() ); // → GET /employees
// Action path: GET /employees/{id} $response = $this->engine->send( actionName: GetEmployeeAction::getName(), context: DefaultActionContext::create(['id' => $id]), ); // → GET /employees/42 \assert($response instanceof GetEmployeeResponse); // GetEmployeeResponse { id: 42, name: 'John Doe', department: 'Engineering' }
// Action path: POST /employees $response = $this->engine->send( actionName: CreateEmployeeAction::getName(), body: CreateEmployeeBody::create(['name' => 'John Doe']), headers: new CorrelationHeaders($correlationId), ); // → POST /employees { "name": "John Doe" } \assert($response instanceof CreateEmployeeResponse); // CreateEmployeeResponse { id: 99, name: 'John Doe', status: 'active' }
// Action endpoint: POST /graphql $response = $this->engine->send( actionName: GetUserAction::getName(), body: GetUserBody::create(['id' => $id]), ); // → POST /graphql { "query": "query { user(id: $id) { name } }", "variables": { "id": 42 } } \assert($response instanceof GetUserResponse); // GetUserResponse { id: 42, name: 'John Doe' }
// Parallel fan-out — one request per employee ID $requests = []; foreach ($ids as $id) { $requests[$id] = EngineRequest::create( GetEmployeeAction::getName(), DefaultActionContext::create(['id' => $id]), ); } $results = $this->engine->sendMany($requests); // BatchResultCollection — each key resolves independently if ($results->hasFailures()) { throw array_values($results->errors())[0]; } return array_map( fn($dto) => Employee::fromDto($dto), $results->responses(), );
For the full pattern (facade → service → domain) → README →
Layered Design
Three levels that emerge naturally. Use whichever you need.
| Class | Responsibility | Scope |
|---|---|---|
| GetEmployeeAction | Only declares the method, path and response DTO. No HTTP logic. | Concrete action |
| MyApiIntegration | Auth, base URL and common headers for MyApi. Reused by all its actions. | Integration |
| AbstractAction | Base contract provided by the engine. Extensible without touching the core. | Bundle |
The make:integration command creates the config, classes and YAML in a single step.
Install in 30 seconds. Ship your first typed integration in under 5 minutes.