Every integration your team ships follows the same predictable standard. New developers understand any existing API in minutes — not days. Automatic OAuth2, parallel requests and typed DTOs built in. One Symfony bundle — distilled from three years of production integrations.
Each new API added without a standard costs your team days to set up and compounds with every new integration. Hardcoded URLs, duplicated OAuth logic, arrays leaking into domain code — the next one is always harder than the last.
Time from zero to a working, tested integration — including OAuth, parallel calls and typed responses.
Scaffold the action, mapper and response for any endpoint. Your whole team generates the same structure, every time.
Installs alongside your existing code. New endpoints follow the standard; legacy integrations migrate at your pace.
sendManyOrFail() dispatches all requests concurrently. Total time ≈ the slowest single request — regardless of how many you send. In production, a Booking.com availability search requires 4 parallel queries for a small city and 17 for Paris — per customer. This handles it.
$requests = []; foreach ($stationIds as $key => $params) { $requests[$key] = EngineRequest::create( actionName: GetStationByIdAction::getName(), context: DefaultActionContext::create($params), ); } // All dispatched concurrently — total time ≈ slowest request $results = $this->engine->sendManyOrFail($requests);
Installs alongside your existing code. No big-bang rewrite — use the pattern for the next new endpoint and migrate legacy at your own pace.
Add the logic and 3 lines to MyApi.yaml — done.
Dynamic auth, batch requests and custom contexts are all in the documentation.
Read the docs →$ php bin/console make:integration MyApi GetUser MyApi/ ├─ MyApi.yaml ← add your endpoint entry here └─ GetUser/ ├─ Request/GetUserAction.php ← HTTP method, path, auth └─ Response/ ├─ GetUserResponse.php ← typed DTO └─ GetUserMapper.php ← raw array → DTO # New endpoint, same integration: $ php bin/console make:integration MyApi CreateOrder # → Adds CreateOrder/ alongside GetUser/. Existing files are never overwritten.What goes inside the generated files? See the docs →
One YAML entry. One mapper. One typed response. OAuth2 token refresh is handled automatically — no token logic in your application code.
GetToken: action: App\...\GetTokenAction method: POST path: /v1/oauth/token CreatePaymentIntent: action: App\...\CreatePaymentIntentAction method: POST path: /v1/payment_intents authorization: type: dynamic action: GetToken token_field: access_token ttl: 3600
Every infrastructure boundary is an interface. Swap the HTTP client, customise path resolution, or add batch support — without touching the engine.
Replace the HTTP client. Tag your implementation and the engine discovers it automatically via Symfony DI.
Complex path logic beyond {placeholders}. Return null to fall back to the default placeholder resolver.
Mark your client as batch-capable for concurrent dispatch. The built-in REST client already implements this.
Built-in test doubles. Test your mappers and actions in isolation — no mocks, no real HTTP required.
Ready to add the pattern to your next project?
The same endpoints, two implementations. Each section shows the real classes from the project.
base_url or add authentication, there is a single point of change.StationDto::fromApiData() is the only point of contact. If the API renames a field, there is exactly one place to fix.RailwayApiService and its private conventions (_hasPhoto). With StationService as the only boundary, controllers only import domain objects and the cost of switching providers is reduced to a single file.sendMany() returns a BatchResultCollection where you inspect each outcome; sendManyOrFail() throws on the first failure after the full batch has run. The default REST client already implements BatchClientInterface via lazy Symfony HttpClient responses — zero additional configuration.I’ve been using this pattern in production for three years — integrating Booking.com, Iberia, Lleego, Hostalia and more. Booking.com availability alone requires 4 parallel queries for a small city, 17 for Paris, per customer. An earlier version of this engine handled that without breaking a sweat. This bundle is what those three years taught me: made explicit, tested, and open.
Drop us a line, open a GitHub Discussion, or install it and give it a try.