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
First Use Case: Webhook Handling
Define a remote event class (e.g., StripePaymentEvent):
// app/Events/StripePaymentEvent.php
namespace App\Events;
use Symfony\Component\RemoteEvent\RemoteEventInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;
class StripePaymentEvent extends RemoteEvent implements RemoteEventInterface
{
public function __construct(
private array $payload,
private string $signature,
private string $expectedSignature
) {}
public function getPayload(): array
{
return $this->payload;
}
public function getSignature(): string
{
return $this->signature;
}
public function getExpectedSignature(): string
{
return $this->expectedSignature;
}
}
Create a Transport Handler (HTTP-based):
// app/Services/RemoteEventTransport.php
use Symfony\Component\RemoteEvent\Transport\RemoteEventTransportInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\RemoteEvent\RemoteEvent;
class HttpRemoteEventTransport implements RemoteEventTransportInterface
{
public function receive(): RemoteEvent
{
$client = HttpClient::create();
$response = $client->request('GET', 'https://example.com/webhook');
return new RemoteEvent(
json_decode($response->getContent(), true),
$response->getHeaders()['x-signature'][0] ?? null
);
}
}
Register in Laravel:
// config/app.php (providers)
Symfony\Component\RemoteEvent\RemoteEventBundle::class,
// config/services.php
'remote_event.transport' => App\Services\HttpRemoteEventTransport::class,
Handle the Event (Laravel-style):
// routes/web.php
use App\Events\StripePaymentEvent;
use Symfony\Component\RemoteEvent\RemoteEventInterface;
Route::post('/webhook', function () {
$event = app()->make(RemoteEventInterface::class);
event(new StripePaymentEvent(
$event->getPayload(),
$event->getSignature(),
'expected_signature_here'
));
});
// app/Interfaces/RemoteEventTransportInterface.php
interface RemoteEventTransportInterface
{
public function receive(): RemoteEventInterface;
public function send(RemoteEventInterface $event): void;
}
bind() in a service provider to resolve transports dynamically:
$this->app->bind(RemoteEventTransportInterface::class, function ($app) {
return new HttpRemoteEventTransport(
$app->make(HttpClient::class)
);
});
Validator (compatible with Laravel’s Validator).use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;
$validator = Validation::createValidator();
$errors = $validator->validate($event, [
new Assert\NotBlank(['message' => 'Payload cannot be empty']),
new Assert\Type(['type' => 'array', 'message' => 'Payload must be an array']),
]);
// app/Middleware/LogRemoteEventMiddleware.php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class LogRemoteEventMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
\Log::info('Remote event received', ['payload' => $request->getParsedBody()]);
return $handler->handle($request);
}
}
app/Http/Kernel.php or use Symfony’s MiddlewareStack.Messenger (compatible with Laravel’s queues).// config/packages/messenger.yaml (Symfony)
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Events\StripePaymentEvent': async
symfony/messenger with Laravel’s queue system:
$this->app->bind(\Symfony\Component\Messenger\MessageBusInterface::class, function ($app) {
$bus = new MessageBus([
new AsyncTransport($app->make(Queue::class)),
]);
return $bus;
});
RetryStrategy for failed event processing.use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\RetryStrategy\RetryStrategyInterface;
$transport = new AsyncTransport($queue, new Serializer(), new RetryStrategy());
RemoteEvent to Laravel’s Event for seamless integration.// app/Services/RemoteEventDispatcher.php
use Symfony\Component\RemoteEvent\RemoteEventInterface;
use Illuminate\Support\Facades\Event;
class RemoteEventDispatcher
{
public function dispatch(RemoteEventInterface $event): void
{
$laravelEvent = new LaravelRemoteEvent($event);
Event::dispatch($laravelEvent);
}
}
ContainerInterface clashes with Laravel’s Container.// app/Providers/RemoteEventServiceProvider.php
use Symfony\Component\DependencyInjection\ContainerInterface;
class RemoteEventServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ContainerInterface::class, function () {
return new LaravelContainerAdapter($this->app);
});
}
}
json_encode/json_decode may not match Symfony’s Serializer.Serializer for consistency:
$serializer = new Serializer([new JsonEncoder()], [new JsonDecoder()]);
$payload = $serializer->decode($event->getContent(), 'json');
SignatureValidator:
use Symfony\Component\RemoteEvent\Validator\SignatureValidator;
$validator = new SignatureValidator('secret_key');
if (!$validator->isValid($event->getSignature(), $event->getPayload())) {
throw new \RuntimeException('Invalid signature');
}
Message objects directly.$serializer = new Serializer();
$message = $serializer->serialize($event, 'json');
Queue::push(new ProcessRemoteEventJob($message));
app/Http/Kernel.php or use Symfony’s MiddlewareStack:
$middlewareStack = new MiddlewareStack();
$middlewareStack->push(new LogRemoteEventMiddleware());
$middlewareStack->push(new ValidateRemoteEventMiddleware());
\Log::debug('Remote event payload', [
'payload' => $event->getPayload(),
'signature' => $event->getSignature(),
'headers' => $event->getHeaders(),
]);
How can I help you explore Laravel packages today?