symfony/remote-event
Symfony RemoteEvent helps your app receive, validate, and handle remote events (like webhooks) in a consistent way. It provides tooling to parse payloads, verify signatures, map to event objects, and process them through Symfony’s event/HTTP workflows.
Install the Package
composer require symfony/remote-event
For Laravel-Symfony interop, also install:
composer require symfony/dependency-injection symfony/http-client symfony/messenger
Create a Basic Remote Event Class Define a DTO (Data Transfer Object) for your event payload:
// app/Domain/Events/StripeWebhook.php
namespace App\Domain\Events;
use Symfony\Component\RemoteEvent\RemoteEventInterface;
class StripeWebhook implements RemoteEventInterface
{
public function __construct(
public string $eventType,
public array $payload,
public ?string $signature = null,
public ?string $idempotencyKey = null
) {}
}
Configure a Transport (HTTP Example)
Use Symfony’s HttpClient to fetch events (or integrate with Laravel’s HttpClient):
// config/remote_event.php
return [
'transports' => [
'stripe' => [
'class' => \Symfony\Component\RemoteEvent\Transport\HttpTransport::class,
'url' => 'https://api.stripe.com/events',
'auth' => [
'method' => 'bearer',
'token' => env('STRIPE_SECRET_KEY'),
],
],
],
];
First Use Case: Webhook Endpoint Create a Laravel controller to receive and process the event:
// routes/web.php
Route::post('/stripe-webhook', [StripeWebhookController::class, 'handle']);
// app/Http/Controllers/StripeWebhookController.php
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\RemoteEvent\Transport\HttpTransport;
class StripeWebhookController extends Controller
{
public function handle(Request $request)
{
$transport = new HttpTransport(
url: 'https://api.stripe.com/events',
auth: ['method' => 'bearer', 'token' => env('STRIPE_SECRET_KEY')]
);
$remoteEvent = new RemoteEvent(
payload: $request->getContent(),
signature: $request->header('Stripe-Signature'),
id: $request->header('Stripe-Idempotency-Key')
);
// Validate and dispatch
$this->validateAndDispatch($remoteEvent);
}
protected function validateAndDispatch(RemoteEvent $event)
{
$validator = $this->container->get('validator');
$errors = $validator->validate($event);
if ($errors->count() > 0) {
abort(400, 'Invalid event');
}
// Dispatch to Laravel's event system
event(new \App\Events\StripeWebhookProcessed($event->getPayload()));
}
}
HTTP Transport: Use Symfony’s HttpClient or Laravel’s HttpClient to fetch events. Wrap it in a service:
// app/Services/RemoteEventTransport.php
use Symfony\Component\RemoteEvent\Transport\HttpTransport;
use Illuminate\Support\Facades\Http;
class RemoteEventTransport
{
public function fetch(string $url, array $headers = []): string
{
$response = Http::withHeaders($headers)->get($url);
return $response->body();
}
public function createHttpTransport(array $config): HttpTransport
{
return new HttpTransport(
url: $config['url'],
auth: $config['auth'] ?? null,
client: $this->createHttpClient($config)
);
}
protected function createHttpClient(array $config): \Symfony\Contracts\HttpClient\HttpClientInterface
{
return \Symfony\Component\HttpClient\HttpClient::create([
'auth_bearer' => $config['auth']['token'] ?? null,
]);
}
}
Queue Transport: For async processing, use Laravel’s queues with Symfony’s Messenger:
// app/Providers/AppServiceProvider.php
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
public function register()
{
$this->app->singleton(MessageBus::class, function ($app) {
$bus = new MessageBus([
new \Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransport(
$app['pestle.connection'],
new Serializer()
),
]);
return $bus;
});
}
Use Symfony’s Validator to validate incoming events:
// app/Domain/Events/StripeWebhook.php
use Symfony\Component\Validator\Constraints as Assert;
class StripeWebhook
{
#[Assert\NotBlank]
public string $eventType;
#[Assert\All({
new Assert\Type('array'),
new Assert\NotBlank()
})]
public array $payload;
}
Integrate with Laravel’s validation:
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($event->toArray(), [
'eventType' => 'required|string',
'payload' => 'required|array',
]);
if ($validator->fails()) {
abort(400, 'Validation failed');
}
Add middleware to transform or log events:
// app/Http/Middleware/LogRemoteEvents.php
use Symfony\Component\RemoteEvent\RemoteEventInterface;
use Psr\Log\LoggerInterface;
class LogRemoteEvents
{
public function __construct(protected LoggerInterface $logger) {}
public function __invoke(RemoteEventInterface $event): RemoteEventInterface
{
$this->logger->info('Remote event received', [
'event_type' => $event->getType(),
'payload' => $event->getPayload(),
]);
return $event;
}
}
Register middleware in Laravel’s pipeline:
// app/Providers/AppServiceProvider.php
use Symfony\Component\RemoteEvent\RemoteEventBus;
public function boot()
{
$bus = $this->app->make(RemoteEventBus::class);
$bus->addMiddleware(new LogRemoteEvents($this->app->make(LoggerInterface::class)));
}
RemoteEvent to Laravel’s Event system:
// app/Services/EventDispatcher.php
use Symfony\Component\RemoteEvent\RemoteEventInterface;
use Illuminate\Support\Facades\Event;
class EventDispatcher
{
public function dispatch(RemoteEventInterface $event)
{
$laravelEvent = new \App\Events\RemoteEventProcessed(
type: $event->getType(),
payload: $event->getPayload()
);
Event::dispatch($laravelEvent);
}
}
queue middleware for retries:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->job(new ProcessRemoteEventJob($event))->everyMinute()->withoutOverlapping();
}
Service Container Conflicts
ContainerInterface expects a different structure than Laravel’s. Use a decorator pattern or service provider to bridge the gap:
// app/Providers/SymfonyContainerProvider.php
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainer;
class SymfonyContainerProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(SymfonyContainer::class, function () {
return new class($this->app) implements SymfonyContainer {
public function __construct(protected $laravelContainer) {}
public function get($id)
{
return $this->laravelContainer->make($id);
}
};
});
}
}
Event Serialization Mismatches
json_encode/json_decode by default, while Symfony may expect SerializerInterface. Normalize serialization:
// app/Services/EventSerializer.php
use Symfony\Component\Serializer\SerializerInterface;
use Illuminate\Support\Facades\JSON;
class EventSerializer
{
public function serialize($data): string
{
return JSON::encode($data);
}
public function deserialize(string $data, string $format, string $type): array
{
return JSON::decode($data, true);
}
}
Idempotency Handling
// app/Models/ProcessedEvent.php
use Illuminate\Database\Eloquent\Model;
class ProcessedEvent extends Model
{
protected $fillable = ['event_id', 'event
How can I help you explore Laravel packages today?