Symfony Bundle · PHP 8.2+ · Packagist
An integration engine for Symfony that centralises your external APIs under clear contracts.
Why not HttpClient?
Use HttpClient for one or two simple calls. Use IntegrationEngine when the API is part of your architecture and you want clear contracts, typed responses, and an Anti-Corruption Layer between the provider and your domain.
The Problem
Different formats, inconsistent authentication, duplicated cache logic. The codebase fragments and every new API means starting from scratch.
Tokens and cache reimplemented in every integration.
Every HTTP client has its own structure.
HTTP coupled to the domain. Impossible to isolate.
Every developer solves the problem their own way.
No traceability, no unified logs, no shared context.
How It Works
A single entry point. Every step has a clear responsibility.
OAuth, sessions, API keys. The engine resolves and caches them automatically.
/orders/{id} is resolved at call time. Explicit failure if a parameter is missing.
YAML → auth → call layer. Each layer overrides the previous. No magic.
Every action defines its own Response DTO with a guaranteed contract.
Client, cache and config source replaceable with one line in YAML.
make:integration generates Mapper, Response DTO and YAML in seconds.
One command generates everything
The command asks the questions. You only write the logic.
$ php bin/console make:integration Github GetUser
The Call Site
No magic strings — everything through contracts.
// Action path: GET /orders $this->engine->send( GetOrdersAction::getName() ); // → GET /orders
// Action path: GET /orders/{id} $response = $this->engine->send( actionName: GetOrderAction::getName(), context: DefaultActionContext::create(['id' => $id]), ); // → GET /orders/42 \assert($response instanceof GetOrderResponse); // GetOrderResponse { id: 42, reference: 'ORD-001', items: [...] }
// Action path: POST /orders $response = $this->engine->send( actionName: CreateOrderAction::getName(), body: CreateOrderBody::create(['reference' => 'ORD-001']), headers: new CorrelationHeaders($correlationId), ); // → POST /orders { "reference": "ORD-001" } \assert($response instanceof CreateOrderResponse); // CreateOrderResponse { id: 99, reference: 'ORD-001', status: 'pending' }
// Action endpoint: POST /graphql $response = $this->engine->send( actionName: GetOrderAction::getName(), body: GetOrderBody::create(['id' => $id]), ); // → POST /graphql { "query": "...", "variables": { "id": 42 } } \assert($response instanceof GetOrderResponse); // GetOrderResponse { id: 42, reference: 'ORD-001', items: [...] }
For the full pattern (facade → service → domain) → README →
Layered Design
Three levels that emerge naturally. Use whichever you need.
| Class | Responsibility | Scope |
|---|---|---|
| CreateChargeAction | Only declares the method, path and response DTO. No HTTP logic. | Concrete action |
| GithubAction | Auth, base path and common GitHub headers. Reused by all GitHub 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.
No boilerplate. No arbitrary decisions. Just your business logic.