IntegrationEngine
Open-source Symfony bundle · Author & maintainer
A common structure for building and maintaining external API integrations in Symfony.
- Requires
- PHP 8.2+ · Symfony 6.4 / 7 / 8
- Licence
- MIT
- Action + Context
- IntegrationEngine
- External API
- Mapper
- Typed Response
Why I built it
Every time I integrated an external API into Symfony, I ended up solving similar pieces again: authentication, HTTP requests, response mapping and error handling. And each integration tended to grow its own conventions. IntegrationEngine came out of that experience.
How it is organised
- Each operation against the external API is an Action.
- A Mapper turns the external response into a typed Response object.
- The bundle handles the shared flow: configuration, authentication and HTTP transport.
- Each integration keeps the provider-specific details where they belong.
A repeatable way of working — clear contracts and a consistent structure — based on problems I have met in production. Providers still differ; the bundle gives those differences a predictable place to live.
Real example: Fetching a movie from TMDB Taken from the demo application
-
Declare the operation
Method, path and mapper for the get_movie action.
get_movie: action: 'App\Integrations\Tmdb\GetMovie\GetMovieAction' method: GET path: /3/movie/{movie_id} mapper: 'App\Integrations\Tmdb\Mappers\GetMovieMapper' -
Call it from the application
A small facade keeps the engine out of controllers and services.
public function getMovie(int $movieId): GetMovieResponse { $engine = $this->registry->get('tmdb'); $context = DefaultActionContext::create(['movie_id' => $movieId]); $response = $engine->send('get_movie', $context); \assert($response instanceof GetMovieResponse); return $response; } -
TMDB answers with its own model
The test fixture the demo uses for this call: 25 fields, most of which the application does not need.
{ "adult": false, "backdrop_path": "/fhvyh6G8M55gkTIPCbeFjvyAh4c.jpg", "belongs_to_collection": null, "budget": 250000000, "genres": [ { "id": 28, "name": "Action" }, { "id": 12, "name": "Adventure" } ], "homepage": "https://www.marvel.com/movies/avengers-endgame", "id": 299536, "imdb_id": "tt4154796", "original_language": "en", "original_title": "Avengers: Endgame", "overview": "After the devastating events that decimated the planet and wiped out half of all life, the Avengers assemble once more in order to reverse Thanos' actions and restore balance to the universe.", "popularity": 408.72, "poster_path": "/or06FN4Hf2tfsWASP2Oa6aSy0l1.jpg", "production_companies": [ { "id": 420, "logo_path": "/hUzeosd33nzE5MCNsZxCGEKTW5l.png", "name": "Marvel Studios", "origin_country": "US" } ], "production_countries": [ { "iso_3166_1": "US", "name": "United States of America" } ], "release_date": "2019-04-26", "revenue": 2798200000, "runtime": 181, "spoken_languages": [ { "english_name": "English", "iso_639_1": "en", "name": "English" } ], "status": "Released", "tagline": "Part of the journey is the end.", "title": "Avengers: Endgame", "video": false, "vote_average": 8.3, "vote_count": 32000 } -
The Mapper keeps what the application uses
Provider field names stay here; the rest of the code only sees the typed response.
protected static function transform(AbstractAction $action, array $response, array $headers): ResponseInterface { /** @var array{id: int, title: string, overview: string, poster_path: string, vote_average: float, release_date: string} $response */ return new GetMovieResponse( id: $response['id'], title: $response['title'], overview: $response['overview'], posterPath: $response['poster_path'], voteAverage: $response['vote_average'], releaseDate: $response['release_date'], ); } -
Typed response
GetMovieResponse::toArray() for the fixture above.
{ "id": 299536, "title": "Avengers: Endgame", "overview": "After the devastating events that decimated the planet and wiped out half of all life, the Avengers assemble once more in order to reverse Thanos' actions and restore balance to the universe.", "poster_path": "/or06FN4Hf2tfsWASP2Oa6aSy0l1.jpg", "vote_average": 8.3, "release_date": "2019-04-26" }
Source: integrationEngine-demo/tree/main/src/Integrations/Tmdb (opens in a new tab)