After the third API, the god class already exists. The inconsistencies already exist. IntegrationEngine doesn’t ask you to rewrite them — it gives you the pattern so the next integration isn’t another problem waiting to happen.
You already know this. Each integration added without a standard has made the next one a little harder to maintain, test, and hand off to another developer.
It might be called StripeService or SalesforceClient. It has 600 lines, three developers have touched it, and nobody wants to add the next endpoint.
Responses travel as array<string, mixed>. Every layer that touches them has to know the exact field names of the API.
The foreach blocks: each request waits for the previous one. 10 items = 10× the time of one. It doesn’t scale, and nothing warns you.
If you know one integration, you know them all. The engine enforces the same shape across every API you integrate.
# RailwayStations.yaml — contract visible at a glance GetStats: action: App\...\GetStatsAction method: GET path: /stats GetStationsByCountry: action: App\...\GetStationsByCountryAction method: GET path: /photoStationsByCountry/{country} GetStationById: action: App\...\GetStationByIdAction method: GET path: /photoStationById/{country}/{stationId}
src/Infrastructure/Integrations/RailwayStations/ ├── RailwayStationsIntegration.php ← facade ├── RailwayStations.yaml ← action map ├── GetStats/ │ ├── Request/GetStatsAction.php │ └── Response/ │ ├── GetStatsResponse.php │ └── GetStatsMapper.php └── GetStationById/ ├── Request/GetStationByIdAction.php └── Response/ ├── GetStationByIdResponse.php └── GetStationByIdMapper.php
# Token fetched once, cached 60 min, retried automatically on 401 GetToken: action: App\...\GetTokenAction method: POST path: /oauth/token GetStationById: action: App\...\GetStationByIdAction method: GET path: /photoStationById/{country}/{stationId} authorization: type: dynamic action: GetToken token_field: access_token ← field in the token response ttl: 3600 ← cached per integration, shared across workers
No big-bang rewrite. IntegrationEngine installs alongside your existing code. Each new API follows the pattern — existing ones migrate only when you choose.
Your existing services and HTTP clients keep working. The bundle adds no runtime dependency on your current integrations.
New integration? Use the pattern. Legacy god class? Leave it until the next feature touches it. No forced cutover.
Every integration lives in its own directory with its own YAML contract. The full surface of an API visible at a glance.
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.| Concept | Without pattern | Engine pattern |
|---|---|---|
| Endpoint declaration | ✗ Scattered across God class methods | ✓ One YAML file per integration |
| URL building | ✗ String concatenation, fails silently | ✓ {placeholders} validated at runtime |
| API fields in code | ✗ Leaked to all layers | ✓ Encapsulated in Mapper + DTO |
| Return type | ✗ array<string, mixed> + _ conventions |
✓ Typed ResponseInterface |
| Anti-Corruption Layer | ✗ None — controller coupled to HTTP client | ✓ StationService as the only boundary |
| Auth (Bearer, Basic, OAuth2) | Manual headers in each request() |
✓ Declared in YAML, managed by the engine |
| Adding a new endpoint | Method + URL + parsing + mapping scattered | ✓ Action + Mapper + Response + 3 YAML lines |
| Batch | ✗ Sequential foreach, linear time |
✓ sendManyOrFail(), constant time |
Drop us a line or join the conversation on GitHub Discussions.