carlosgude/integration-engine
IntegrationEngine forces every Symfony integration into the same predictable shape: each endpoint is just a Request and a Response, with facades, mappers, and YAML action maps generated for you. No ad-hoc HTTP clients or scattered mapping logic.
Framing correction. The brief labels this a "Laravel/PHP package." It is not. carlosgude/integration-engine is a Symfony bundle: it self-registers via Symfony Flex, is configured through config/packages/integration_engine.yaml, ships bin/console make:integration and debug:integration commands, discovers adapters and middleware through Symfony DI service tags, and requires Symfony 6.4+ (7.x, 8.x). If our estate is Laravel-first, out-of-the-box fit is zero and the real decision is "do we fund a Laravel bridge?" rather than "do we adopt this package?" If we are on Symfony, it is a native, low-friction integration.
Architecture fit. The package enforces a genuinely useful discipline: every external API becomes a directory of endpoints, each endpoint is exactly one Action (request) + one Response + one Mapper, wired by a YAML action map and exposed through a thin facade ({Name}Integration) plus an Anti-Corruption-Layer Gateway that is the only class aware of both the vendor DTOs and our domain model. That is a textbook ports-and-adapters shape and fits cleanly into a hexagonal or DDD-flavoured Symfony codebase with a dedicated integration/infrastructure layer. It fits poorly where teams want a thin generic HTTP wrapper, where integration logic is intentionally co-located with application services, or where the domain model is already the transport model.
Integration feasibility. Low for a Symfony team: composer require, scaffold one integration, define base URL and client type, register in YAML, done. The README plus DOCUMENTATION.md, a working demo repo (REST, GraphQL, dynamic OAuth2), ADRs and an upgrade guide are a reasonable on-ramp for a package at this maturity. Escape hatches remain credible — custom adapters via ClientAdapterInterface, custom middleware via AbstractClientMiddleware, custom clients via client_service, project adapters overriding built-ins — so we adopt a pattern and a small runtime, not a black box.
What v9.4.0 changes (and does not). The release is small and additive. It restores a pre-mapping timing split: a new ResponseReceived lifecycle event fires once the client returns a response and before mapping, in send() (static and dynamic auth) and per sendMany() item. ResponseMapped::timestamp − ResponseReceived::timestamp gives mapping time; ResponseReceived followed by RequestFailed means "the API answered, our mapper failed." This is a meaningful narrowing of the observability gap flagged below — it is now possible to separate vendor slowness from our own mapping slowness without bolting on our own timers. It also ships a real bug fix: debug:integration <name> no longer fails with "Unable to inspect actions" on integrations that declare the v9.3.0 defaults: section. The rest is documentation: a upgrade-9.0.md guide for the v9.0.0 context-only path resolution change, and corrections to examples that did not compile (lowercase Symfony HttpClient header names, maxAttempts vs maxRetries on ExponentialBackoff, a GraphQL body missing create()), plus a webhook doc fix noting an accepted webhook event is dispatched to Messenger before the 202, so with the default synchronous bus the consumer runs inside the webhook request.
Technical risk. High, and mostly non-technical:
ResponseReceived is explicitly a restoration of the pre-mapping split that v7's HttpResponseReceived provided and that the v8 event rework dropped. Three majors, three different shapes of the same observability hook. Treat the event API as unstable at major boundaries and do not let it become load-bearing for billing, SLAs or alerting without an internal adapter.ResponseReceived is not emitted for transport failures or for the 401 that triggers a token refresh — so the exact requests you most want to see in an auth-degradation incident are invisible in this split. No documented timeout, retry/backoff or circuit-breaker defaults.cache.app is process-local under PHP-FPM, so every worker fetches its own OAuth token; strict token-endpoint rate limits require a shared Redis pool and multi-connection integrations cache per connection. Documented, but easy to get wrong at scale — and now harder to detect, per the previous bullet.sendMany() is only truly concurrent when the client implements BatchClientInterface; the REST client does, GraphQL does not. GraphQL fan-out silently degrades. v9.4.0 emits ResponseReceived per batch item, which gives us per-item transport timing to prove or disprove this in staging.debug:integration fix suggests the tooling is being exercised, but hand edits will still drift and there is no documented validation-at-boot guarantee.hasFailures()/errors(), and DTO shape drift are ours to solve.Key questions.
bodyClass, profiler_payloads opt-in are documented — confirm they are off by default in production)?test client adapter?Stack fit.
| Stack | Fit | Notes |
|---|---|---|
| Symfony 6.4–8.x, PHP 8.2+ | Native | Bundle, Flex recipe, DI tags, make:/debug: commands all work as advertised |
| Symfony with legacy Guzzle clients | Good | Needs a ClientAdapterInterface shim; built-ins target Symfony HttpClient |
| Laravel / Lumen | Poor | Requires a service provider, config publishing, container wiring, command port and removal of bundle assumptions |
| Framework-agnostic PHP services | Partial | Core engine reusable; all wiring must be hand-built |
| Mixed estate | Per-service | No cross-language story; this is PHP-only |
Migration path. Treat this as a strangler around existing clients, not a rewrite. v9.4.0 improves the middle of this path, not the ends.
make:integration, implement facade + gateway, keep the legacy client live behind a feature flag. Verify debug:integration <name> against a YAML file that uses defaults: — that was broken until this release.ResponseReceived and ResponseMapped here: the delta between them is the fastest way to attribute a parity failure to transport or to our mapper.body: path-parameter extraction that strips consumed keys from the payload, which is a nice touch but must be verified against each API.dynamic OAuth2 with a shared Redis cache_service and verify the 401-evict-and-retry-once path in a chaos test. Do not rely on ResponseReceived to observe this path — it is deliberately silent there.sendMany() only where the client supports BatchClientInterface; bound batch size and test worker memory. Confirm per-item ResponseReceived timings show real concurrency before declaring a batch path "done."Gateway may import an integration facade or its DTOs.Compatibility. Verify at spike time: PHP 8.2+ floor against our oldest supported service; Symfony minor version matrix; whether it coexists with our existing HTTP client configuration (proxy, mTLS, base timeouts); compatibility with our static analysis level (typed class constants pin PHP 8.3+ in places); config cache warm-up behaviour in CI; and whether Composer constraints permit our Symfony version without --with-all-dependencies churn. Add one item this cycle: if we already consume lifecycle events, re-check any assertion of an exact event sequence — ResponseReceived now sits between RequestSent and ResponseMapped, which is additive but will break order-sensitive listeners and tests.
Sequencing. Spike (1–2 days, now including event-ordering and profiler review) → architecture decision (1 day) → pilot + parity (3–5 days) → middleware baseline: timeouts, retry/backoff, rate limiting, structured logging (3–5 days, ours to build) → token caching hardening (2–3 days) → rollout waves (1–3 days per integration once conventions exist). Laravel bridge, if required, is a separate 8–15 day workstream with ongoing maintenance burden. Add ~1 day to consume v9.4.0's timing events into the parity harness and a first-cut latency dashboard.
Adoption of the upgrade. Low risk: ResponseReceived is additive, and the debug:integration fix is a bug fix. Pin to 9.4.* only after confirming no listener in our codebase assumes a two-event (RequestSent → ResponseMapped) sequence, and read docs/upgrade-9.0.md even though we are starting at 9.x — it documents the context-only path resolution change that any copied sample code predating v9.0.0 will get wrong.
Maintenance. Low code volume, high ownership concentration. Plan to pin exact versions (to the minor, e.g. 9.4.*, given an active patch cadence), keep a fork-and-patch capability, and budget roughly one engineer-day per quarter for dependency bumps and upstream review. If we build middleware, test doubles or a Laravel bridge, we become the de facto maintainer of that surface with no upstream guarantee it won't be broken. Treat the lifecycle-event API as version-fragile: it has now changed shape at v7 → v8 → v9, and our internal adapters should be the only consumers.
Support. Community-only: GitHub issues, docs, ADRs and a demo repo. No SLA, no documented security disclosure policy, no LTS. Mitigation is internal: a named owner, an internal "integration standards" guide built on top of this pattern, and a decision log. Escalation to the maintainer is not a viable plan. The documentation and ADR hygiene visible in this release raises confidence in comprehensibility but not in continuity.
Scaling. Three concrete limits. (1) Token endpoint pressure — cache.app is per-worker under FPM, so N workers means N token fetches on cold start; move to shared Redis and consider a lock to prevent stampede. (2) Fan-out memory and socket pressure — cap batch size, verify the REST adapter's concurrency ceiling, and note GraphQL cannot parallelise at all. (3) Rate limiting across workers — a per-process limiter in middleware is not sufficient; use a shared store. Beyond that, throughput is bounded by Symfony HttpClient, which is fine for typical B2B integration volumes and wrong for high-QPS public API proxying.
Failure modes. Watch for: upstream schema drift surfacing as mapper exceptions (no schema validation layer) — now partially diagnosable via ResponseReceived → RequestFailed; token revocation mid-flight (one retry, then hard failure) with no ResponseReceived emitted, so auth-refresh latency and volume are a blind spot in any dashboard built on the new event; token endpoint 429 cascades at scale-out; partially successful batches where callers ignore hasFailures(); missing default timeouts starving PHP-FPM workers; YAML/PHP action-map drift producing "unknown action" at runtime rather than boot; middleware ordering bugs silently disabling retry or auth; secrets leaking into logs or the Symfony profiler panel absent explicit redaction — check profiler_payloads is not enabled in any shared environment; and, new this release, order-sensitive event listeners or tests breaking on the inserted ResponseReceived event, or on the defaults:-section YAML shape the fixed debug:integration now tolerates.
Ramp-up. A mid-level Symfony developer scaffolds their first integration in under a day and is productive within a week, because the pattern is small, uniform and demonstrated by the demo repo. The real learning curve is cultural, not technical: the facade-plus-gateway rule is the whole value proposition and is trivially bypassed if reviews don't enforce it. Budget an extra two to three days per team for middleware conventions and test fixtures — and allow half a day for the timing-event semantics, since "ResponseReceived absent" has three distinct meanings (transport failure, 401 refresh, event not wired) that a new team will get wrong once. Treat "no direct registry or facade calls from controllers" as a lint-level rule.
Recommendation. Unchanged: conditional go for Symfony services on PHP 8.2+. Adopt on a pilot with the gateway boundary enforced, shared-cache tokens, a self-built observability/retry middleware baseline, pinned versions and a named internal owner. v9.4.0 makes this marginally more attractive — it closes part of the observability gap we called the second-largest functional hole, and the debug:integration fix removes a first-week papercut — but it does not move adoption, testability, or support risk, and the event-surface history across three majors reinforces that we must own the observability layer rather than depend on it. No-go for Laravel-first estates unless a Laravel bridge is separately funded and staffed, and no-go as a platform-wide standard until at least two integrations have run in production for a quarter.
How can I help you explore Laravel packages today?