Symfony Bundle · PHP 8.2+ · Packagist

External integrations tend to rot in Symfony projects

Every API becomes a different shape, a different structure, a different way of thinking. After a few months, you no longer have integrations. You have a zoo. IntegrationEngine forces every integration to look the same.

Copied! composer require carlosgude/integration-engine
View on GitHub →

HttpClient sends requests. IntegrationEngine structures integrations.

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.

Every integration ends up as an isolated case

Inconsistent API clients, scattered HTTP logic, repeated mapping boilerplate. The codebase fragments and every new API means starting from scratch.

Inconsistent clients

Every API ends up with its own HTTP client and its own structure.

Ad-hoc HTTP logic

HTTP calls scattered across services with no shared contract.

Mapping boilerplate

The same transformation code written from scratch per endpoint.

"How does this API work?"

No predictable layout means archaeology every time you touch an integration.

Starting from scratch

Every new API is a new decision. No structure carries over.

A single flow for all your integrations

A single entry point. Every step has a clear responsibility.

Registry
IntegrationEngine
Action
Auth
HTTP
Mapper
Response DTO

Dynamic auth with cache

OAuth, sessions, API keys. The engine resolves and caches them automatically.

Path context

/employees/{id} is resolved at call time. Explicit failure if a parameter is missing.

Headers in three layers

YAML → auth → call layer. Each layer overrides the previous. No magic.

Typed responses

Every action defines its own Response DTO with a guaranteed contract.

Batch / parallel requests

sendMany() runs N requests concurrently. Individual failures never abort the batch.

Scaffolding included

make:integration generates Mapper, Response DTO and YAML in seconds.

From zero to a typed integration in seconds.

The command asks the questions. You only write the logic.

$ php bin/console make:integration MyApi GetEmployee
config/packages/integration_engine.yaml
src/Infrastructure/Integrations/MyApi/MyApiIntegration.php
src/Infrastructure/Integrations/MyApi/MyApi.yaml
src/Infrastructure/Integrations/MyApi/GetEmployee/Request/GetEmployeeAction.php
src/Infrastructure/Integrations/MyApi/GetEmployee/Response/GetEmployeeMapper.php
src/Infrastructure/Integrations/MyApi/GetEmployee/Response/GetEmployeeResponse.php

One line. Always the same.

No HTTP clients. No request builders. No mappers. Just your integration's facade.

// 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 →

The bundle proposes. It does not impose.

Three levels that emerge naturally. Use whichever you need.

ClassResponsibilityScope
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.

Get started in one command

No boilerplate. No arbitrary decisions. Just your business logic.