Elegant integration between Symfony applications and n8n workflow automation platform.
- Type-safe communication using PHP interfaces
- UUID tracking system for request/response pairing
- Flexible communication modes: Fire & Forget, Async with callback, Sync
- Robust error handling with retry and circuit breaker
- Event-driven architecture for monitoring and logging
- Multi-instance support for different environments
- Dry run mode for testing without actual sending
composer require freema/n8n-bundle# Install Task (https://taskfile.dev)
brew install go-task/tap/go-task
# Initialize development environment
task init
# Start dev server
task serve
# Show available commands
task --list# config/packages/n8n.yaml
n8n:
clients:
default:
base_url: 'https://your-n8n-instance.com'
client_id: 'my-symfony-app'
auth_token: '%env(N8N_AUTH_TOKEN)%'
timeout_seconds: 30
retry_attempts: 3
enable_circuit_breaker: true
proxy: '%env(HTTP_PROXY)%' # Optional: HTTP proxy URL<?php
use Freema\N8nBundle\Contract\N8nPayloadInterface;
use Freema\N8nBundle\Contract\N8nResponseHandlerInterface;
use Freema\N8nBundle\Enum\RequestMethod;
class ForumPost implements N8nPayloadInterface
{
public function toN8nPayload(): array
{
return [
'text' => $this->content,
'author_id' => $this->authorId,
'created_at' => $this->createdAt->format(DATE_ATOM)
];
}
public function getN8nContext(): array
{
return [
'entity_type' => 'forum_post',
'entity_id' => $this->id,
'action' => 'moderate'
];
}
// Optional: define HTTP method and content type
public function getN8nRequestMethod(): RequestMethod
{
return RequestMethod::POST_FORM; // or POST_JSON, GET, etc.
}
// Optional: custom response handler
public function getN8nResponseHandler(): ?N8nResponseHandlerInterface
{
return new ModerationResponseHandler();
}
// Optional: response entity mapping
public function getN8nResponseClass(): ?string
{
return ModerationResponse::class;
}
}<?php
// Fire & Forget - returns response data immediately
$result = $n8nClient->send($post, 'workflow-id');
// $result = ['uuid' => '...', 'response' => [...], 'mapped_response' => object, 'status_code' => 200]
// Async with callback
$uuid = $n8nClient->sendWithCallback($post, 'workflow-id', $responseHandler);
// Sync
$result = $n8nClient->sendSync($post, 'workflow-id');Sends data to n8n and returns immediate response from webhook.
$result = $n8nClient->send($payload, 'workflow-id');
// Returns: ['uuid' => '...', 'response' => {...}, 'mapped_response' => object|null, 'status_code' => 200]Sends data + callback URL, n8n processes and returns result.
class MyResponseHandler implements N8nResponseHandlerInterface
{
public function handleN8nResponse(array $responseData, string $requestUuid): void
{
// Process response from n8n
}
public function getHandlerId(): string
{
return 'my_handler';
}
}
$uuid = $n8nClient->sendWithCallback($payload, 'workflow-id', new MyResponseHandler());Waits for immediate response (if n8n webhook supports it).
$result = $n8nClient->sendSync($payload, 'workflow-id', 30); // 30s timeoutBundle supports various HTTP methods and content types:
use Freema\N8nBundle\Enum\RequestMethod;
class MyPayload implements N8nPayloadInterface
{
public function getN8nRequestMethod(): RequestMethod
{
return RequestMethod::POST_FORM; // Form data (application/x-www-form-urlencoded)
// return RequestMethod::POST_JSON; // JSON body (application/json)
// return RequestMethod::GET; // GET parameters
// return RequestMethod::PUT_JSON; // PUT with JSON
// return RequestMethod::PATCH_FORM; // PATCH with form data
}
}You can automatically map n8n responses to PHP objects:
// 1. Create response entity
class ModerationResponse
{
public function __construct(
public readonly bool $allowed,
public readonly ?string $reason = null,
public readonly ?string $confidence = null
) {}
}
// 2. Specify class in payload
class ForumPost implements N8nPayloadInterface
{
public function getN8nResponseClass(): ?string
{
return ModerationResponse::class;
}
}
// 3. Use mapped object
$result = $n8nClient->send($post, 'workflow-id');
$mappedResponse = $result['mapped_response']; // Instance of ModerationResponse
$isAllowed = $mappedResponse->allowed; // Type-safe accessBundle includes debug panel for Symfony Web Profiler:
# config/packages/n8n.yaml
n8n:
debug:
enabled: true # or null for auto-detection based on kernel.debug
log_requests: truePanel shows:
- All N8n requests with UUID, duration, status
- Payload data and response data
- Errors and their details
- Total request count and time
Bundle emits events for each phase of communication:
// Event listener
class N8nMonitoringListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
N8nRequestSentEvent::NAME => 'onRequestSent',
N8nResponseReceivedEvent::NAME => 'onResponseReceived',
N8nRequestFailedEvent::NAME => 'onRequestFailed',
N8nRetryEvent::NAME => 'onRetry',
];
}
public function onRequestSent(N8nRequestSentEvent $event): void
{
// Log, metrics, monitoring...
}
}The dev/ directory contains a test application with Docker support:
- Start Docker container:
task up- Install dependencies:
task init- Environment configuration:
# Copy example file
cp dev/.env.example dev/.env.local
# Edit dev/.env.local and fill in:
# - N8N_WEBHOOK_FIRE_AND_FORGET - your webhook ID
# - N8N_WEBHOOK_WITH_CALLBACK - your webhook ID for callback
# - N8N_CALLBACK_BASE_URL - URL where your application runs (for callback)- Start development server:
task serveApplication will be available at http://localhost:8080
# Environment management
task init # Initialize development environment
task up # Start Docker containers
task down # Stop Docker containers
task restart # Restart environment
task logs # Show logs
# Development
task serve # Start dev server
task shell # Shell into dev container
# N8n testing
task test:health # Health check
# Testing
task test # Run PHPUnit tests
task stan # PHPStan analysis
task cs:fix # Fix code stylePOST /demo/fire-and-forget- Fire & Forget testPOST /demo/with-callback- Async callback testPOST /demo/sync- Synchronous testGET /demo/health- Health checkPOST /api/n8n/callback- Callback endpointGET /_profiler- Symfony Web Profiler
// 1. Post implements N8nPayloadInterface
class ForumPost implements N8nPayloadInterface
{
public function toN8nPayload(): array
{
return [
'text' => $this->content,
'author_id' => $this->authorId,
'thread_id' => $this->threadId
];
}
}
// 2. Handler for processing results
class ForumPostModerationHandler implements N8nResponseHandlerInterface
{
public function handleN8nResponse(array $responseData, string $requestUuid): void
{
$status = $responseData['status']; // 'ok', 'suspicious', 'blocked'
$spamScore = $responseData['spam_score'];
$flags = $responseData['flags'];
// Based on result: publish/block/send for manual review
match($responseData['suggested_action']) {
'approve' => $this->approvePost($postId),
'manual_review' => $this->queueForManualReview($postId, $flags),
'block' => $this->blockPost($postId, $flags)
};
}
}
// 3. Usage
$post = new ForumPost(/*...*/);
$handler = new ForumPostModerationHandler();
$uuid = $n8nClient->sendWithCallback($post, 'moderation-workflow-id', $handler);For testing your application without making actual HTTP requests to n8n, use the MockN8nClient:
<?php
use Freema\N8nBundle\Testing\MockN8nClient;
class MyServiceTest extends TestCase
{
private MockN8nClient $mockN8n;
protected function setUp(): void
{
$this->mockN8n = new MockN8nClient();
}
public function testForumPostModeration(): void
{
// Configure the mock response
$this->mockN8n->willReturn([
'status' => 'approved',
'spam_score' => 0.1,
'confidence' => 'high'
]);
// Test your service
$service = new ModerationService($this->mockN8n);
$result = $service->moderatePost($forumPost);
// Verify the request was sent
$this->mockN8n->assertSent('moderation-workflow-id');
$this->mockN8n->assertSentCount(1);
// Verify payload content
$this->mockN8n->assertSentWithPayload('moderation-workflow-id', [
'text' => 'Forum post content'
]);
}
}Configure responses:
// Single response
$mockClient->willReturn(['status' => 'ok']);
// Multiple responses in sequence
$mockClient->willReturnSequence([
['status' => 'pending'],
['status' => 'completed'],
]);
// Simulate exceptions
$mockClient->willThrow(new N8nCommunicationException('Connection failed', 500));Assertions:
// Assert request was sent
$mockClient->assertSent('workflow-id');
// Assert with custom callback
$mockClient->assertSent('workflow-id', function (array $request) {
return $request['payload']->getSomeValue() === 'expected';
});
// Assert request was not sent
$mockClient->assertNotSent('workflow-id');
// Assert number of requests
$mockClient->assertSentCount(3);
// Assert nothing was sent
$mockClient->assertNothingSent();
// Assert payload data
$mockClient->assertSentWithPayload('workflow-id', [
'key' => 'expected-value'
]);Inspect requests:
// Get all requests
$requests = $mockClient->getRequests();
// Get requests for specific workflow
$requests = $mockClient->getRequestsFor('workflow-id');
// Reset state between tests
$mockClient->reset();Configure behavior:
// Custom client ID
$mockClient->withClientId('test-client');
// Health status
$mockClient->withHealthStatus(false);n8n:
clients:
default:
base_url: 'https://n8n.example.com'
client_id: 'my-app'
auth_token: '%env(N8N_AUTH_TOKEN)%'
timeout_seconds: 30
retry_attempts: 3
retry_delay_ms: 1000
enable_circuit_breaker: true
circuit_breaker_threshold: 5
circuit_breaker_timeout_seconds: 60
dry_run: false
proxy: 'http://proxy.example.com:3128' # Optional HTTP proxy
default_headers:
X-Custom-Header: 'My App'
staging:
base_url: 'https://staging.n8n.example.com'
client_id: 'my-app-staging'
dry_run: true
callback:
route_name: 'n8n_callback'
route_path: '/api/n8n/callback'
tracking:
cleanup_interval_seconds: 3600
max_request_age_seconds: 86400MIT