carlosgude/integration-engine
IntegrationEngine standardizes external API integrations in Symfony: each endpoint is a predictable Action with a Request and Response DTO + Mapper. Includes scaffolding commands to generate integrations and actions, keeping HTTP/mapping logic consistent and maintainable.
ClientMiddlewareInterface + AbstractClientMiddleware as
the extension point for cross-cutting concerns (caching, tracing, rate limiting, etc.)MiddlewareClient wraps every HTTP adapter in an ordered chain; always implements
BatchClientInterface and DynamicBaseUrlClientInterfaceCachingMiddleware replaces the former caching decoratorsTracingMiddleware replaces the former tracing decorators (debug only)integration_engine.middleware and an optional priority attribute
(higher priority = outermost layer, same convention as Symfony event listeners)TraceableClient and TraceableBatchClient — superseded by TracingMiddlewareCachingMiddleware (outermost) →
user middlewares by descending priority → TracingMiddleware (debug only) →
HTTP adapterfix: ensure TraceableClient and TraceableBatchClient implement DynamicBaseUrlClientInterface
TraceableClient and TraceableBatchClient failed to support DynamicBaseUrlClientInterface, breaking per-request base URL resolution in kernel.debug mode.Two additive, opt-in capabilities — no breaking changes, no new required dependencies.
Dynamic base URL per request
For integrations without one fixed host — a multitenant app where each store/customer lives on its own domain (Shopify-style). IntegrationEngine::send() and sendMany() now accept an optional baseUrl argument:
$engine->send('GetOrders', context: $context, baseUrl: $tenant->domain());
- New DynamicBaseUrlClientInterface — opt-in via instanceof, checked at runtime. A client that doesn't implement it ignores baseUrl silently.
- Implemented by both built-in adapters: SymfonyHttpClientAdapter, GraphQLClientAdapter.
- sendMany() groups batch requests by resolved baseUrl before dispatch, so a batch mixing several target hosts still runs each group concurrently through BatchClientInterface instead of degrading to sequential sends.
- Omit baseUrl and behavior is identical to every prior version — the configured base_url is used, exactly as before.
Symfony Profiler / Debug Toolbar integration
Every outgoing call made through a configured integration now shows up automatically in the Symfony Toolbar/Profiler in dev/test — integration name, action, HTTP method, path, duration, and status or error. No configuration needed.
- TraceableClient / TraceableBatchClient decorate ClientInterface/BatchClientInterface — IntegrationEngine itself is untouched.
- Wired by IntegrationCompilerPass only when all three hold: kernel.debug is true, symfony/http-kernel's DataCollectorInterface is available, and a profiler service is actually registered (i.e. symfony/web-profiler-bundle is installed and active). In any other case — including all of prod — the real client is used unwrapped, zero overhead.
- symfony/http-kernel added to require-dev only — still zero new dependencies in require.
Docs
- README: "Dynamic base URL per request", "Symfony Profiler integration"
- ARCHITECTURE.md: §8 (why base_url is opt-in), §9 (why a decorator, not engine instrumentation)
- New docs/debugging.md
Quality
- 305 tests, 636 assertions — full suite green, including no-break regression coverage for both features.
- PHPStan max — no errors.
- Verified end-to-end against a real consuming app: two distinct live hosts for dynamic baseUrl, and the profiler panel rendering real outgoing calls (REST + GraphQL) including a real upstream error case.
Internal (no user-facing change, worth knowing if you touch IntegrationCompilerPass)
- Classes excluded from Symfony's service autodiscovery are registered as abstract placeholder definitions (tag container.excluded), not simply absent — hasDefinition() alone can't tell the difference from a real one.
- Compiler pass execution order across bundles matters: IntegrationCompilerPass now runs at priority 10 (before FrameworkBundle's ProfilerPass) so the data collector tag exists before the profiler's tagged-service scan runs.
DynamicAuthHandler extracted from IntegrationEngine — token resolution and caching logic now lives in a dedicated class, improving separation of concernsAction/, Auth/, Client/, Mapper/, Response/) for cleaner importsIntegrationGeneratorException introduced — generator filesystem errors now throw a dedicated exception instead of \RuntimeExceptionPathResolutionException::pcreError() named constructor addedFakeLogger now handles non-string log levels gracefullyfeat: introduce DynamicAuthHandler for improved token handling and logging
DynamicAuthHandler for centralized dynamic auth token resolution, caching, and 401 token retries.BatchTokenRetry updates.IntegrationEngine to delegate dynamic auth logic to DynamicAuthHandler.DispatchedRequest for simplifying request dispatch tracking in SymfonyHttpClientAdapter.DynamicAuthHandler in integrations.ResolvesAuthHeaders.fix: ensure FakeLogger handles non-string log levels gracefully
FakeLogger to set an empty string for non-string log levels, ensuring type consistency.
fix: ensure FakeLogger handles non-string log levels gracefullyFakeLogger to set an empty string for non-string log levels, ensuring type consistency.ulti request (https://github.com/CarlosGude/integrationEngine/pull/4)
sendMany and BatchResultSetsendMany() method that returns keyed BatchResultSet objects for consolidated response handling.BatchResult to encapsulate individual request outcomes, distinguishing success and failure.EngineRequest and PreparedRequest classes to enable immutable packaging of batch requests and pre-processed requests, respectively.BatchClientInterface for concurrent batch request execution; implements fallback to sequential processing when unavailable.BATCH-CONSOLIDATION.md.AbstractBatchMapper for consolidated DTO responses in homogeneous batch actions.AbstractBatchMapper, BatchResultCollection, and batch mapper supportAbstractBatchMapper for consolidated DTO responses in homogeneous batch actions.BatchResultCollection to encapsulate batch processing results with action-based validation.mapWith method for routing collections through mappers.BatchMapperActionMismatchException to enforce batch action invariants.IntegrationEngine::sendMany to return BatchResultCollection for unified handling of batch responses.BatchSendSadPathTest to validate partial results, action mismatches, and retry behavior.DynamicAuthSadPathTest to test token invalidation, custom headers, and non-401 error handling.GraphQLClientAdapterBodyTest for query serialization, data extraction, and adapter-specific behavior.GraphQLClientAdapterErrorTest to validate GraphQL and HTTP error handling in payload and response codes.refactor: replace token retry logic in IntegrationEngine with BatchTokenRetry for improved clarity and modularity
feat: introduce BatchTokenRetry to manage token caching and 401 retry logic
BatchTokenRetry class for handling dynamic auth tokens in batch processing.BATCH-CONSOLIDATION.md, update batch docs and diagramsBATCH-CONSOLIDATION.md to reflect completed batch mapper implementation and prevent confusion.batch-requests.md, clients.md) with finalized API and behavior.class-graph.md) to incorporate batch components and processing flow.TESTING.md with batch test coverage details and execution guidelines for batch-specific suites.infection-per-mutator.md and infection-summary.log as they are no longer maintained.delete() to CachePort and Psr6CacheAdapter (key
sanitization consistent with get/set).X-Tenant
→ X_Tenant issue).DynamicAuthorizationConfig::prefix is now nullable with smart
defaults:
Authorization → defaults to Bearerapi_key handling to correctly respect configured prefixes
(including YAML static configs).ContainerBuildermake:integration via CommandTesterFakeClient with queued exceptions for realistic HTTP failure
simulationdocs/class-graph.md with Mermaid diagrams:
resolvePathCallback--memory-limit=1G in Makefile for improved
analysis stability.CachePort interface now requires:delete(string $key): void
Bug fixes
resolvePath was a required method on a public interface (ActionContextInterface) v1.26.0 added resolvePath(string $path): ?string as a mandatory method on ActionContextInterface. Any existing implementation would crash with a fatal error after composer update. The method has been removed from the base interface and moved to the new optional PathResolvableContextInterface.
defaultAuthHeaders() declared private in the trait prevented overriding The docblock stated the method could be overridden in consuming classes, but PHP's private-method binding silently ignored any class-level override. Visibility is now protected.
Constructor-injected Accept header was silently discarded In both SymfonyHttpClientAdapter and GraphQLClientAdapter, the array_merge order caused resolveHeaders() (which bundled defaultAuthHeaders()) to overwrite constructor-supplied headers. An adapter configured with $defaultHeaders = ['Accept' => 'application/xml'] would always send application/json. Priority order is now correct: defaultAuthHeaders < $defaultHeaders < auth headers < per-request headers.
getPath() returned an empty string without throwing If resolvePath() returned "", the null !== $resolved guard let it through and the HTTP adapter built a malformed URL. PathResolutionException::resolverReturnedEmptyPath() is now thrown instead.
resolveHeaders() mixed auth logic with default headers The ResolvesAuthHeaders trait always seeded the header array with $this->defaultAuthHeaders() before adding auth headers, coupling two responsibilities and making priority control impossible from outside. resolveHeaders() now returns only authentication headers (Bearer / Basic / ApiKey).
Unsafe offset access on mixed in GraphQLClientAdapter (phpstan level max) GraphQL error message extraction accessed $data['errors'][0]['message'] directly without type guards, failing phpstan at level max. Extraction is now type-safe with explicit is_array checks at each level.
New
PathResolvableContextInterface New optional interface (extends ActionContextInterface) for contexts that need full control over path resolution. Returning null from resolvePath() delegates to the default {placeholder} resolver.
class MyContext implements PathResolvableContextInterface { public function resolvePath(string $path): ?string { return '/v2' . $path; // return null to fall back to placeholder resolution } }
PathResolutionException::resolverReturnedEmptyPath() New exception factory thrown when resolvePath() returns "".
Migration from v1.26.0
If you implemented resolvePath() on your context class to customize path resolution, change implements ActionContextInterface to implements PathResolvableContextInterface — no other changes needed.
If you implemented resolvePath() only to satisfy the interface (returning null), remove the method entirely.
Internal
feat: add context-driven path resolution support
resolvePath() method in ActionContextInterface to enable customizable path resolution logic.DefaultActionContext and FakeContext to include resolvePath() with default implementations.AbstractAction to rely on context's resolvePath() method for path resolution, falling back to default logic if null is returned.resolverDidNotReturnString case.tests/Core/ExceptionMessagesTest.php — verifies every exception constructs the
correct message. Kills MethodCallRemoval and Concat mutants across all seven
exception classes.tests/Infrastructure/GraphQLClientAdapterHeadersTest.php — covers all three auth
types (bearer, basic, api_key), the default arm, the += merge semantics,
the prefix ?? 'Bearer' coalesce, and the early return when no auth is present.tests/Infrastructure/GraphQLClientAdapterTest.php — extended with statusCode
assertions on body-validation and GraphQL error paths.tests/Infrastructure/SymfonyHttpClientAdapterBodyTest.php — covers POST/PUT/PATCH
body serialisation, GET without body, >= 400 boundary at exactly 400, \Throwable
network error wrapping, RequestResponseException rethrow, 204 empty response, and
whitespace-only body.tests/Infrastructure/SymfonyHttpClientAdapterResolveHeadersTest.php — mirrors the
GraphQL header tests for the REST adapter: basic, api_key, default, +=
preservation, LogicalAnd token guard, and the prefix ?? 'Bearer' coalesce.infection.json5 — Infection configuration with minMsi: 90 and minCoveredMsi: 94.infection.json5 thresholds raised to match current test quality:
minMsi: 90, minCoveredMsi: 94.The remaining 6% of escaped mutants are infrastructure-level false positives:
getContent(throw: false) guards, \Throwable network error catch blocks, and a
CastString on a scalar value that passes through a is_scalar() check two lines
above. No production logic was modified — all improvements were test additions only.
config_path is now validated at container compile time via
IntegrationConfigurationException instead of throwing a generic
\InvalidArgumentException from the compiler pass.action field is validated at YamlConfigAdapter construction time,
before the PHPDoc cast, producing a descriptive error on misconfigured files.has() removed from CachePort interface and Psr6CacheAdapter — the
method was not used by the engine and was a leftover from a previous refactor.fix: enforce config_path validation at compile time, update exception handling, and revise related test/documentation
IntegrationRegistry — Integration names consisting only of whitespace (e.g. ' ') passed the empty-string guard and were registered silently under an unreachable key. The guard now uses trim().DOCUMENTATION.md / DOCUMENTATION_ES.md — Added warning to the config_path section: the file is resolved at runtime, not at compile time. A missing path will not be caught by cache:warmup.DOCUMENTATION.md / DOCUMENTATION_ES.md — Added limitation note to the dynamic auth section: the basic auth type is incompatible with the dynamic auth flow. Use a custom ClientInterface instead.The details that only show up in production.
IntegrationEngine — Dynamic auth cache keys were not scoped to the integration name. Two integrations with a token action of the same name (e.g. FetchToken) shared the same cache key integration_engine.token.FetchToken, causing one integration to authenticate silently with another's token. Keys are now scoped: integration_engine.token.{integrationName}.{actionName}.IntegrationEngine — $integrationName: string added to the constructor. The IntegrationCompilerPass passes the integration name at wiring time. IntegrationEngineTestCase updated accordingly.IntegrationEngine::applyMapper() — Added explanatory comment clarifying why the MapperActionMismatchException guard is intentionally duplicated from AbstractMapper::map(): the engine-side guard is a fail-fast for misconfigured custom ConfigPort or ClientInterface implementations; the mapper-side guard is the public contract for callers outside the engine flow.DynamicAuthorizationConfig — New prefix field (default 'Bearer'). APIs using non-Bearer Authorization schemes (e.g. Authorization: Token abc123, Authorization: Digest ...) can now be configured without a custom ClientInterface. The prefix is forwarded through StaticAuthorizationConfig params and applied by both SymfonyHttpClientAdapter and GraphQLClientAdapter.DOCUMENTATION.md / DOCUMENTATION_ES.md:
bearer row notes that the prefix is configurable via prefix:.prefix: added as a commented optional field with explanation.{integrationName}.{actionName} format.\w+ limitation for hyphenated placeholders (e.g. {user-id}) with a ready-to-use resolvePathCallback() example using {([^}]+)}.$e->statusCode and $e->context corrected from the nonexistent getStatusCode() / getContext() getter methods.DynamicAuthTest — 7 → 8 tests. Added dynamicAuthUsesCustomPrefixInAuthorizationHeader: verifies that prefix: 'Token' in DynamicAuthorizationConfig is carried through to StaticAuthorizationConfig params after token resolution.DynamicAuthTest — Cache key assertions updated to the new integration_engine.token.test_integration.{action} format.Lo que nadie lee hasta que algo falla.
SymfonyHttpClientAdapter — DELETE requests no longer send a body. DELETE was listed alongside POST, PUT, and PATCH in the body serialisation check despite IntegrationContext::hasBody() explicitly excluding it. The adapter now matches the contract.Configuration — The client: key validation was hardcoded to ['rest', 'graphql'], silently rejecting any custom adapter type registered via ClientAdapterInterface. Validation now only enforces non-empty — the compiler pass is the right place to catch unknown types.Psr6CacheAdapter — Key sanitisation now includes . alongside the PSR-6 reserved characters. The engine generates keys like integration_engine.token.{action} — some pools (Redis, APCu) reject dots. The sanitiser no longer lies about being complete.RequestResponseException — The HTTP status code was being passed as the second argument to RuntimeException::__construct(), polluting $e->getCode() with HTTP semantics. The exception already exposes $statusCode as a public property — getCode() now returns 0 as intended.TemplateRenderer::body() — Dead method. Generated a ActionBodyInterface skeleton that was never called from IntegrationFileGenerator. Removed without ceremony.IntegrationContext — Removed isGet(), isPost(), isPut(), isDelete(), isPatch(). Five helpers that existed because a language model thought they looked useful. hasBody() and hasResponse() inline the comparisons directly — no intermediate layer needed.DOCUMENTATION.md, DOCUMENTATION_ES.md, agent-guide.md) — DELETE removed from the list of methods that send a body.DOCUMENTATION.md, DOCUMENTATION_ES.md, agent-guide.md) — Corrected from the nonexistent InMemoryCacheAdapter to Psr6CacheAdapter wrapping cache.app.DOCUMENTATION.md, DOCUMENTATION_ES.md) — "always take precedence" softened to reflect that it depends on registration order, not a framework guarantee.GraphQLBodyInterface docblock and agent guide — The file_get_contents example no longer implies built-in .graphql file support. Both inline and file-based patterns are shown as user-side choices.Psr6CacheAdapterTest — 8 tests covering get/set/has and key sanitisation, including the dot regression (integration_engine.token.*).TESTING.md — Directory tree and suite documentation updated to reflect the full Infrastructure test suite: ClientAdapterResolverTest, GraphQLClientAdapterTest, Psr6CacheAdapterTest, SymfonyHttpClientAdapterHeadersTest.CHANGELOG — v1.23.0 "The Interpreter" Core
Added ClientAdapterInterface — extends ClientInterface with getClientType(), requiresPath() and requiresMethod(). The contract for autodiscoverable adapters Added GraphQLBodyInterface — extends ActionBodyInterface with getQuery() and getVariables(). Queries can be declared inline or loaded from .graphql files via file_get_contents()
Infrastructure
Added GraphQLClientAdapter — built-in GraphQL adapter. Always POST to the endpoint URL, serialises body as { query, variables }, extracts data[] before the mapper, detects GraphQL errors in HTTP 200 responses and throws RequestResponseException Added ClientAdapterResolver — registry of adapter classes keyed by getClientType(). Built after compile time from tagged services. Later registrations override earlier ones — project adapters always win over bundle built-ins SymfonyHttpClientAdapter now implements ClientAdapterInterface with requiresPath(): true and requiresMethod(): true
Bundle
Configuration — new client key per integration: "rest" (default), "graphql", or any registered custom type. client_service still overrides everything IntegrationCompilerPass — builds ClientAdapterResolver from integration_engine.client_adapter tagged services. Two guards before registration: class_exists() and is_a(ClientAdapterInterface). Fills the resolver service via addMethodCall so the command can use it at runtime MakeIntegrationCommand — action argument now optional. Client type chosen from registered adapters dynamically. Path and method questions driven by requiresPath() and requiresMethod() — no hardcoded type comparisons. client: soap appears in the list as soon as the adapter is registered IntegrationContext — replaced isGraphQL() with adapterRequiresPath and adapterRequiresMethod capabilities. Protocol-agnostic TemplateRenderer — YAML entry and action scaffold driven by adapter capabilities, not by type string
Tests
Added GraphQLClientAdapterTest — 15 tests covering body serialisation, endpoint URL, data extraction, GraphQL errors in 200, HTTP errors, invalid body, headers, auth, and ClientAdapterInterface capabilities Added ClientAdapterResolverTest — 7 tests covering registration, resolution, project-over-bundle override, full map, empty resolver, and custom adapter
Documentation
DOCUMENTATION.md / DOCUMENTATION_ES.md — Quick start GraphQL example, scaffolding table REST vs GraphQL, client: key in YAML config, GraphQLBodyInterface with file_get_contents pattern, extensibility section with full ClientAdapterInterface example including requiresPath and requiresMethod, two new error entries TESTING.md — GraphQLClientAdapterTest and ClientAdapterResolverTest suites documented integration-engine-agent-guide.md — ClientAdapterInterface (3.7) and GraphQLBodyInterface (3.8) sections added, client: vs client_service: FAQ entry
Tests
Extracted shared engine setup into IntegrationEngineTestCase — DynamicAuthTest and EngineContractTest extend it, eliminating duplicated setUp() and property declarations Moved all reusable fakes to tests/Fake/ — FakeCache, FakeClient, FakeConfigPort, FakeContext, FakeTokenAction, FakeTokenMapper, FakeTokenResponse, FakePathAction, FakeProtectedAction Replaced all anonymous ActionContextInterface classes with FakeContext::create([...]) Replaced EngBasicResponse (inline duplicate of FakeTokenResponse) with FakeTokenResponse in EngineContractTest Updated AbstractActionTest to expect PathResolutionException instead of \RuntimeException — tests now protect the actual contract FakePathAction and FakeProtectedAction simplified to hasResponse: false — these actions are used to verify auth and context propagation, not response mapping
Documentation
Added TESTING.md — full guide to the test suite: philosophy, structure, per-test rationale, fakes inventory, and base class explanation README.md updated with reference to TESTING.md
Housekeeping
Removed TestingIdeas.md — superseded by TESTING.md Removed phpunit.xml.dist.bak — leftover from --migrate-configuration Removed empty directories src/Bundle/Generator/OpenApi/ and src/Bundle/Generator/Schema/ Added .DS_Store to .gitignore Removed tracked artefacts from repo: .DS_Store, .idea/, .php-cs-fixer.cache, .phpunit.result.cache
Tests
Added EngineContractTest — engine contract suite: full flow, empty response, ActionNotFoundException, NotMappedActionException, MapperActionMismatchException, context propagation to client
Documentation
Restructured DOCUMENTATION.md — new section order from quick start to deep reference, Anti-Corruption Layer pattern documented explicitly, complete Action/Mapper/Response examples, dynamic auth end-to-end example Added DOCUMENTATION_ES.md — full Spanish translation Updated README.md — quick start first, correct usage pattern with facade and application service Updated integration-engine-agent-guide.md — aligned with current contracts, generic templates, removed incorrect hasBody() reference, added ACL pattern
Housekeeping
Removed InvalidMapperException — declared but never thrown Removed InvalidMethodException — declared but never thrown phpunit.xml.dist — added coverage exclusions for interfaces and ports, enabled testdox output .php-cs-fixer.dist.php — disabled php_unit_test_class_requires_covers to prevent deprecated #[CoversNothing] injection, added setUnsupportedPhpVersionAllowed(true)
AbstractAction no longer stores context. Previously, context was injected
into the action via withContext() and retrieved via getActionContext(),
which meant the action carried execution state. This was conceptually wrong —
an action is a contract that defines an operation, not a call. Execution state
belongs to the engine.
The refactor removes three things from AbstractAction:
private ?ActionContextInterface $contextwithContext(?ActionContextInterface): staticgetActionContext(): ?ActionContextInterfaceAnd changes getPath() to accept the context as a parameter:
// Before
$action->withContext($context)->getPath();
// After
$action->getPath($context);
The same action instance can now be called with different contexts in successive requests without cloning or mutation:
$action->getPath(DefaultActionContext::create(['id' => 1])); // /orders/1
$action->getPath(DefaultActionContext::create(['id' => 2])); // /orders/2
The engine passes context as an explicit parameter through send() →
applyAuthorization() → client->send(). The action never sees its own
context — it only resolves the path when the engine asks it to, with the
context the engine provides at that moment.
This eliminates the regression risk from applyAuthorization() accidentally
dropping the context when reconstructing the action with a resolved token.
ClientInterface and SymfonyHttpClientAdapter updatedsend() now accepts ?ActionContextInterface $context as a second parameter:
public function send(
AbstractAction $action,
?ActionContextInterface $context = null,
?RequestHeadersInterface $headers = null,
): array;
The adapter resolves the path at HTTP call time: $action->getPath($context).
AbstractAction::withContext() removed. Any code calling this method
must be updated to pass context directly to getPath() or to send().
AbstractAction::getActionContext() removed. Context is no longer
accessible from outside the engine.
ClientInterface::send() signature changed. Custom ClientInterface
implementations must add ?ActionContextInterface $context = null as the
second parameter.
If you have custom ClientInterface implementations, update the send()
signature:
public function send(
AbstractAction $action,
?ActionContextInterface $context = null, // ← add this
?RequestHeadersInterface $headers = null,
): array;
If you have custom resolvePathCallback() implementations that called
withContext() internally, update them to use the context parameter passed
directly to the callback:
protected function resolvePathCallback(): callable
{
return function (string $path, ?ActionContextInterface $context): string {
// $context is passed directly — no withContext() needed
return $resolvedPath;
};
}
DefaultActionContext — built-in context implementationThe bundle now ships with a general-purpose ActionContextInterface
implementation that eliminates boilerplate for the common case:
use IntegrationEngine\Core\Contract\DefaultActionContext;
$registry->get('orders_api')->send(
actionName: 'GetOrder',
context: DefaultActionContext::create(['id' => 42]),
);
Previously, every project had to write its own context class even for simple
key-value parameter passing. DefaultActionContext handles this without any
custom code.
The contract stays uniform — DefaultActionContext implements
ActionContextInterface — so existing custom context classes continue to work
unchanged. DefaultActionContext is the default; custom classes are for
when the context needs validation, type coercion, or domain logic.
// Before — required a custom class for every parameter
context: GetOrderContext::create(['id' => $id])
// After — built-in for simple cases
context: DefaultActionContext::create(['id' => $id])
assert() pattern for typed responsesThe documentation now covers the recommended pattern for narrowing
ResponseInterface to a concrete type without triggering PHPStan errors:
public function getOrder(int $id): GetOrderResponse
{
$response = $this->engine->send(
actionName: GetOrderAction::getName(),
context: DefaultActionContext::create(['id' => $id]),
);
\assert($response instanceof GetOrderResponse);
return $response;
}
assert() satisfies PHPStan's type narrowing, signals intent clearly, and
has zero cost in production when assertions are disabled.
No breaking changes. DefaultActionContext is a new addition — nothing
existing is affected.
Custom context classes continue to work as before. Migrating them to
DefaultActionContext is optional and only makes sense when the class has
no logic beyond key-value storage.
Una mesa no se cae.
This release focuses on developer experience: the scaffolding command now works out of the box with no prior setup, and the documentation covers the full surface of the bundle.
make:integration no longer requires a pre-existing
config/packages/integration_engine.yaml. On first run the command asks for
the integration base URL and creates the config file automatically.
The command now also asks for the action path and HTTP method interactively, so the generated YAML entry is complete and correct from the start:
fix instalation issues
chore: update composer.json to define Symfony version requirement and register IntegrationEngineBundle
Resolves a set of misconfiguration issues in the Symfony bundle layer that prevented integrations from being registered in the container. The core engine was not affected. All fixes are isolated to the bundle's dependency injection setup and the code generator.
Changelog All notable changes to carlosgude/integration-engine will be documented in this file. The format is based on Keep a Changelog.
[Unreleased] Fixed
IntegrationEngineBundle — the IntegrationCompilerPass was never registered because IntegrationEngineBundle did not override build(). Integrations configured under integration_engine.integrations were silently ignored and any call to IntegrationRegistry::get() threw IntegrationNotFoundException. IntegrationCompilerPass — base_url was read from configuration but never passed to the HTTP client. Each integration's SymfonyHttpClientAdapter is now constructed with its own base_url, one service definition per integration (integration_engine.http_client.{name}). The phantom alias integration_engine.http_client.default has been removed. IntegrationEngine::send() — removed the $yamlUrl argument. The config path is now injected into YamlConfigAdapter at compile time via the CompilerPass; there is no reason to pass it again at runtime. Call sites are simplified to ->send('ActionName') or ->send('ActionName', $body). YamlConfigAdapter — config loading moved from setYaml() (called on every send()) to the constructor (called once at container compile time). The setYaml() method and its corresponding ConfigPort interface method have been removed. services.yaml — added explicit integration_engine.cache.default service definition pointing to InMemoryCacheAdapter. Added exclude entries for YamlConfigAdapter, SymfonyHttpClientAdapter, and IntegrationEngine to prevent Symfony's autowire scan from trying to resolve their dynamic constructor arguments automatically. MakeIntegrationCommand — the integration namespace was built by concatenating $name onto $baseNamespace before passing it to IntegrationContext, causing every generated namespace to include the integration name twice (e.g. App\Integrations\RickAndMorty\RickAndMorty...). The command now passes $baseNamespace as-is; IntegrationContext::integrationNamespace() appends $name internally. IntegrationFileGenerator — generateIntegrationFiles() included the YAML config file in its output, while appendActionToConfig() unconditionally appended the same entry afterward. The first action was always written twice. The YAML file is now managed exclusively by appendActionToConfig(). TemplateRenderer::yamlEntry() — heredoc indentation caused leading whitespace to be included in the generated YAML, producing structurally invalid files. The method now builds the entry as a plain interpolated string.
Changed
IntegrationRegistry is now the only entry point for consumers. The IntegrationEngine instance is no longer intended to be injected directly into application services or controllers. ConfigPort interface no longer declares setYaml().
How can I help you explore Laravel packages today?