Symfony Bundle · PHP 8.2+ · Packagist

After a few months, you no longer have integrations. You have a zoo.

IntegrationEngine forces every integration to look the same. Ship faster. Onboard instantly. Stop doing archaeology forever.

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

HttpClient is a tool. IntegrationEngine is a standard.

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.

Every integration ends up as an isolated case

Without a shared standard, each developer solves it differently. The codebase fragments. Knowledge disappears. Every new API means starting from scratch.

Days lost per API

No shared pattern means days of discovery and arbitrary decisions every time a new API lands on the backlog.

Onboarding that never scales

A different structure per integration means every new teammate has to start from zero, every time.

Bugs hiding in the gaps

HTTP logic scattered across services, no contract to enforce, nothing to test in isolation.

Zero reuse

Auth, caching, mapping — reinvented from scratch every single time, in every integration.

Nobody knows how it works

Six months later, the developer who built it is gone and the code is unreadable.

One entry point. One contract. Every time.

A single flow for all your external APIs. Every step has a clear, testable owner.

Registry
IntegrationEngine
Action
Auth
HTTP
Mapper
Response DTO

Ship in minutes, not days

make:integration scaffolds Action, Mapper, Response DTO and YAML in one command. You write only business logic.

If you know one, you know them all

Every integration follows the same layout and the same contracts. Instant onboarding for every new teammate.

Token management, zero effort

OAuth, sessions, API keys — fetched, cached, and auto-refreshed on 401. No manual token logic, ever.

Type-safe responses

Every endpoint returns a guaranteed DTO. No guessing at runtime, no silent surprises in production.

Parallel requests, built-in

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

Swap any layer

HTTP client, cache backend, config source — each replaceable with one line in YAML.

From zero to a production-ready integration in one command.

Answer three questions. Get a fully scaffolded, typed integration.

$ 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

The same call. Every time.

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 →

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.

Standardise your integrations today.

Install in 30 seconds. Ship your first typed integration in under 5 minutes.