Eventos
Introdução
Os eventos do Laravel proporcionam uma simples implementação de padrões observers, permitindo que você assine e ouça vários eventos ocorridos em sua aplicação. Normalmente, as classes de evento são armazenadas na pasta app/Events
, enquanto os seus ouvintes são armazenados na app/Listeners
. Não se preocupe caso você não veja essas pastas em sua aplicação, pois eles serão criados para você ao gerar eventos e ouvintes usando comandos Artisan no console.
Os eventos servem como uma excelente maneira de desacoplar os vários aspectos da sua aplicação, pois um único evento pode ter múltiplos ouvintes que não dependem uns dos outros. Por exemplo, você pode querer enviar uma notificação do Slack para seu usuário a cada vez que uma encomenda for expedida. Em vez de acoplar o código de processamento de ordem ao código de notificação no Slack, você pode criar um App\Events\OrderShipped
evento que possa ser recebido por um ouvintes e utilizado para enviar uma notificação do Slack.
Gerando eventos e ouvinte
Para gerar eventos e ouvinte de maneira rápida é possível utilizar os comandos: make:event
e make:listener
do Artisan:
php artisan make:event PodcastProcessed
php artisan make:listener SendPodcastNotification --event=PodcastProcessed
Por conveniência, você também pode invocar os comandos Artisan make:event
e make:listener
sem argumentos adicionais. Quando você fizer isso, o Laravel solicitará automaticamente o nome da classe e, ao criar um listener, qual evento ele deve ouvir:
php artisan make:event
php artisan make:listener
Registro de eventos e ouvintes
Descoberta de eventos
Por padrão, o Laravel irá encontrar e registrar automaticamente seus ouvintes de eventos escaneando o diretório Listeners
da sua aplicação. Quando o Laravel encontra qualquer método de classe de ouvinte que comece com handle
ou __invoke
, o Laravel registrará esses métodos como ouvintes de eventos para o evento que é sugerido por tipo (type-hint) na assinatura do método:
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* Lidar com o evento determinado.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}
Se você planeja armazenar seus ouvinte em um diretório diferente ou em vários diretórios, poderá instruir o Laravel a procurar esses diretórios usando o método withEvents
no arquivo bootstrap/app.php
do seu aplicativo:
->withEvents(discover: [
__DIR__.'/../app/Domain/Listeners',
])
O comando event:list
pode ser usado para mostrar todos os eventos registrados na aplicação:
php artisan event:list
Descoberta de eventos em produção
Para acelerar seu aplicativo, você deve armazenar em cache um manifesto de todos os controladores do aplicativo usando os comandos optimize
ou event:cache
. Tipicamente, esse comando deve ser executado como parte do processo de implantação da sua aplicação. Esse manifesto será usado pelo framework para acelerar o processo de registro de eventos. O comando event:clear
pode ser utilizado para destruir o cache de eventos.
Registro manual de eventos
Usando a facade Event
, você pode registrar eventos e seus respectivos ouvinte manualmente no método boot
de seu aplicativo AppServiceProvider
:
use App\Domain\Orders\Events\PodcastProcessed;
use App\Domain\Orders\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
/**
* Inicialize qualquer serviço de aplicativo.
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
}
O comando event:list
pode ser usado para listar todos os atalhos registrados em seu aplicativo:
php artisan event:list
Ouvidores de closure
Normalmente, os eventos são definidos como classes; contudo, você pode também registrar manualmente eventos baseados em closures no método boot
do AppServiceProvider
do aplicativo:
use App\Events\PodcastProcessed;
use Illuminate\Support\Facades\Event;
/**
* Inicialize qualquer serviço de aplicativo.
*/
public function boot(): void
{
Event::listen(function (PodcastProcessed $event) {
// ...
});
}
Ouvintes de eventos anônimos com fila
Ao registrar um ouvinte de eventos baseados em closure, você pode envolver o closure do ouvinte dentro da função Illuminate\Events\queueable
para instruir o Laravel a executar o ouvinte usando filas:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
/**
* Inicialize qualquer serviço de aplicativo.
*/
public function boot(): void
{
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
}));
}
Tal como acontece com os trabalhos em fila, você pode usar os métodos onConnection
, onQueue
e delay
para personalizar a execução do ouvinte em fila.
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));
Se você quiser lidar com falhas anônimas de ouvintes enfileirados, você pode fornecer um closure para o método catch
enquanto define o ouvinte queueable
. Este closure receberá a instância do evento e a instância Throwable
que causou a falha do ouvinte:
use App\Events\PodcastProcessed;
use function Illuminate\Events\queueable;
use Illuminate\Support\Facades\Event;
use Throwable;
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
// O ouvinte na fila falhou...
}));
Ouvintes de eventos com caractere wildcard
É também possível registrar ouvintes usando o caractere *
como parâmetro de substituição, permitindo que este recebam vários eventos no mesmo ouvinte. Os ouvintes de substituição recebem o nome do evento como primeiro argumento e todo o array de dados do evento como segundo argumento:
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});
Definindo Eventos
Uma classe de evento é essencialmente um recipiente de dados que guarda as informações relacionadas ao evento. Suponhamos que um evento App\Events\OrderShipped
receba um objeto do Eloquent ORM:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Crie uma nova instância de evento.
*/
public function __construct(
public Order $order,
) {}
}
Como você pode ver, essa classe de evento não contém nenhuma lógica. Ela é um recipiente para a instância App\Models\Order
que foi comprada. A trait SerializesModels
, usado pelo evento, graciosamente serializa qualquer modelo Eloquent se o objeto do evento for serializado usando a função serialize
do PHP, como no caso da utilização de ouvintes agendados.
Definindo Ouvintes
Em seguida, vamos dar uma olhada no ouvinte do nosso exemplo de evento. Os ouvintes recebem instâncias de eventos em seu método handle
. O comando do Artisan make:listener
, quando invocado com a opção --event
, importará automaticamente as classes de eventos adequadas e indicarão o tipo do evento no método handle
. Dentro do método handle
, você poderá executar quaisquer ações necessárias para responder ao evento:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* Crie o ouvinte de evento.
*/
public function __construct()
{
// ...
}
/**
* Lidar com o evento.
*/
public function handle(OrderShipped $event): void
{
// Acesse o pedido usando $event->order...
}
}
NOTA
Seus ouvintes de eventos também podem sugerir qualquer dependência necessária em seus construtores. Todos os ouvintes de eventos são resolvidos através do container de serviço do Laravel, portanto as dependências serão injetadas automaticamente.
Interromper a propagação de um evento
Por vezes, poderá desejar interromper a propagação de um evento para outros leitores. Pode fazê-lo retornando false
da função handle
do seu leitor.
Ouvintes de eventos em fila
Ouvir em fila é benéfico se o usuário precisar executar uma tarefa lenta, como enviar um e-mail ou fazer um pedido HTTP. Antes de usar os ouvintes em fila, configure a fila e inicie um assistente de fila no servidor ou no ambiente de desenvolvimento local.
Para especificar que um ouvinte deve ser priorizado em uma fila de espera, adicione a interface ShouldQueue
à classe do listener. Os ouvintes gerados pelos comandos Artisan make:listener
já importam essa interface no namespace atual para que você possa usá-la imediatamente:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}
É isso! Agora, quando um evento gerenciado por este ouvinte for enviado, o ouvinte será automaticamente agendado pelo gerente de eventos usando o sistema de fila. Se nenhuma exceção for lançada quando o ouvinte for executado na fila, o trabalho pendente será automaticamente excluído depois que tiver sido concluído o processamento.
Personalizar a Conexão da Fila, Nome e Demora
Se você quiser personalizar a conexão da fila, o nome da fila ou o tempo de atraso da fila de um ouvinte de evento, você pode definir as propriedades $connection
, $queue
ou $delay
em sua classe de ouvinte:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* O nome da conexão para a qual o trabalho deve ser enviado.
*
* @var string|null
*/
public $connection = 'sqs';
/**
* O nome da fila para a qual o trabalho deve ser enviado.
*
* @var string|null
*/
public $queue = 'listeners';
/**
* O tempo (segundos) antes do trabalho ser processado.
*
* @var int
*/
public $delay = 60;
}
Se você preferir definir a conexão da fila do ouvinte, o nome da fila ou o tempo de atraso na execução, poderá definir os métodos viaConnection
, viaQueue
ou withDelay
no ouvinte:
/**
* Obtenha o nome da conexão da fila do ouvinte.
*/
public function viaConnection(): string
{
return 'sqs';
}
/**
* Obtenha o nome da fila do ouvinte.
*/
public function viaQueue(): string
{
return 'listeners';
}
/**
* Obtenha o número de segundos antes que o trabalho seja processado.
*/
public function withDelay(OrderShipped $event): int
{
return $event->highPriority ? 0 : 60;
}
Enfileiramento condicional de ouvintes
Às vezes, você pode precisar determinar se um listener deve ser agendado com base em alguns dados disponíveis apenas na execução. Para isso, uma método shouldQueue
pode ser adicionado para determinar se o listener deve ser agendado. Se o método shouldQueue
retornar false
, o listener não será agendado:
<?php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardGiftCard implements ShouldQueue
{
/**
* Recompense um vale-presente ao cliente.
*/
public function handle(OrderCreated $event): void
{
// ...
}
/**
* Determine se o ouvinte deve ser colocado na fila.
*/
public function shouldQueue(OrderCreated $event): bool
{
return $event->order->subtotal >= 5000;
}
}
Interação manual com a fila
Se você precisar acessar manualmente os métodos delete
e release
da fila subjacente do ouvinte, você poderá fazer isso usando o trait Illuminate\Queue\InteractsWithQueue
. Esse trait é importado por padrão em ouvintes gerados e fornece acesso a esses métodos:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Lidar com o evento.
*/
public function handle(OrderShipped $event): void
{
if (true) {
$this->release(30);
}
}
}
Ouvintes de eventos enfileirados e transações de banco de dados
Quando os eventos agendados são enviados para a execução dentro de transações do banco de dados, estes podem ser processados pela fila antes da transação no banco de dados ter sido confirmada. Neste caso, quaisquer alterações efetuadas nos modelos ou registros no banco de dados durante a transação do banco de dados podem ainda não ser refletidas na base de dados. Além disso, os modelos ou registros criados dentro da transação do banco de dados poderão não existir na base de dados. Se o seu evento agendado depender destes modelos, poderão ocorrer erros inesperados quando o trabalho que envia o evento agendado a ser executado é processado.
Se a opção de configuração do comando de conclusão da conexão da fila estiver definida como false
, ainda é possível indicar que um determinado ouvinte de fila deve ser distribuído após todas as transações de banco de dados em aberto terem sido concluídas implementando a interface ShouldHandleEventsAfterCommit
na classe do ouvinte:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit
{
use InteractsWithQueue;
}
NOTA
Para saber mais sobre como solucionar esses problemas, consulte a documentação sobre tarefas enfileiradas e transações de banco de dados.
Tratamento de trabalhos com falha
Às vezes, os eventos em fila de espera podem falhar. Se o número máximo de tentativas definido para a sua tarefa exceder a capacidade da fila, o método failed
será chamado no seu ouvinte. O método failed
recebe uma instância do evento e um Throwable
que causou a falha:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Throwable;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Lidar com o evento.
*/
public function handle(OrderShipped $event): void
{
// ...
}
/**
* Lidar com uma falha de trabalho.
*/
public function failed(OrderShipped $event, Throwable $exception): void
{
// ...
}
}
Especificando o número máximo de tentativas do ouvinte em fila
Se um de seus ouvintes na fila estiver encontrando algum erro, é provável que você não queira que ele tente novamente de forma indefinida. Portanto, o Laravel oferece várias maneiras de especificar quantas vezes ou por quanto tempo um ouvinte pode ser repetido.
Você pode definir uma propriedade $tries
em sua classe de ouvinte para especificar quantas vezes este pode ser tentado novamente antes que ele seja considerado falhado:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* O número de vezes que o listener enfileirado pode ser tentado.
*
* @var int
*/
public $tries = 5;
}
Como alternativa para definir o número de vezes que um ouvinte é repetido antes de falhar, você pode definir a hora em que o ouvinte não será mais executado. Isso permite que os testes sejam realizados quantas vezes forem necessários dentro do prazo. Para definir a hora até a qual o ouvinte não deve mais ser repetido, crie uma método retryUntil
em sua classe de ouvinte. Esse método deverá retornar uma instância DateTime
:
use DateTime;
/**
* Determine o horário em que o ouvinte deve atingir o tempo limite.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
Despachando eventos
Para enviar um evento, você pode chamar o método estático dispatch
. Este método é disponibilizado ao evento pela trait Illuminate\Foundation\Events\Dispatchable
. Quaisquer argumentos passados para o método dispatch
serão passados para o construtor do evento:
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class OrderShipmentController extends Controller
{
/**
* Envie o pedido determinado.
*/
public function store(Request $request): RedirectResponse
{
$order = Order::findOrFail($request->order_id);
// Lógica de envio do pedido...
OrderShipped::dispatch($order);
return redirect('/orders');
}
}
Se você quiser enviar condicionalmente um evento, poderá utilizar os métodos dispatchIf
e dispatchUnless
:
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);
NOTA
Ao testar, pode ser útil afirmar que determinados eventos foram despachados sem realmente acionar seus ouvintes. Os ajudantes de teste integrados do Laravel tornam isso muito fácil.
Enviando eventos após transações de banco de dados
Às vezes, você pode querer instruir o Laravel para despachar um evento somente após a transação ativa no banco de dados ser confirmada. Para fazer isso, você pode implementar a interface ShouldDispatchAfterCommit
na classe do evento.
Esta interface instrui o Laravel a não despachar o evento até que a transação atual do banco de dados seja confirmada. Se a transação falhar, o evento será descartado. Se nenhuma transação de banco de dados estiver em andamento quando o evento for despachado, o evento será despachado imediatamente:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderShipped implements ShouldDispatchAfterCommit
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Crie uma nova instância de evento.
*/
public function __construct(
public Order $order,
) {}
}
Assinantes de eventos
Escrevendo assinantes de eventos
Os assinantes de evento são classes que podem se inscrever em múltiplos eventos dentro da própria classe assinante, permitindo definir vários manipuladores de evento dentro de uma única classe. O método subscribe
do assinante deve ser chamado com uma instância do evento dispatcher. Você pode chamar o método listen
no dispatcher passado para registrar os ouvintes de eventos:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* Lidar com eventos de login do usuário.
*/
public function handleUserLogin(Login $event): void {}
/**
* Lidar com eventos de logout do usuário.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Registre os ouvintes do assinante.
*/
public function subscribe(Dispatcher $events): void
{
$events->listen(
Login::class,
[UserEventSubscriber::class, 'handleUserLogin']
);
$events->listen(
Logout::class,
[UserEventSubscriber::class, 'handleUserLogout']
);
}
}
Se os métodos do ouvinte de evento estiverem definidos no próprio assinante, poderá ser mais conveniente retornar um array de eventos e nomes dos métodos no seu método subscribe
. O Laravel irá automaticamente determinar o nome da classe do assinante ao registrar os ouvintes de eventos:
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserEventSubscriber
{
/**
* Lidar com eventos de login do usuário.
*/
public function handleUserLogin(Login $event): void {}
/**
* Lidar com eventos de logout do usuário.
*/
public function handleUserLogout(Logout $event): void {}
/**
* Registre os ouvintes do assinante.
*
* @return array<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}
}
Registrando assinantes de eventos
Depois de escrever o assinante, você está pronto para registrá-lo com o despachante de evento. Você pode registrar os assinantes usando o método subscribe
da facade Event
. Tipicamente, isso deve ser feito dentro do método boot
do seu AppServiceProvider
:
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Inicialize qualquer serviço de aplicativo.
*/
public function boot(): void
{
Event::subscribe(UserEventSubscriber::class);
}
}
Teste
Ao testar um código que dispache eventos, você pode querer instruir o Laravel para não executar os controladores de eventos, já que o código dos mesmos pode ser testado diretamente e separadamente do código que dispacha o evento correspondente. Claro está que, para testar o próprio controlador, é possível instanciá-lo e chamar o método handle
diretamente no seu teste.
Usando o método fake
da facade Event
, você pode impedir que os ouvinte executem suas ações, e então, testar quais eventos foram enviados pela sua aplicação usando as métricas assertDispatched
, assertNotDispatched
e assertNothingDispatched
:
<?php
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
test('orders can be shipped', function () {
Event::fake();
// Realizar envio do pedido...
// Afirmar que um evento foi despachado...
Event::assertDispatched(OrderShipped::class);
// Afirmar que um evento foi despachado duas vezes...
Event::assertDispatched(OrderShipped::class, 2);
// Afirmar que um evento não foi despachado...
Event::assertNotDispatched(OrderFailedToShip::class);
// Afirme que nenhum evento foi despachado...
Event::assertNothingDispatched();
});
<?php
namespace Tests\Feature;
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* Envio do pedido de teste.
*/
public function test_orders_can_be_shipped(): void
{
Event::fake();
// Realizar envio do pedido...
// Afirme que um evento foi despachado...
Event::assertDispatched(OrderShipped::class);
// Afirmar que um evento foi despachado duas vezes...
Event::assertDispatched(OrderShipped::class, 2);
// Afirmar que um evento não foi despachado...
Event::assertNotDispatched(OrderFailedToShip::class);
// Afirme que nenhum evento foi despachado...
Event::assertNothingDispatched();
}
}
Pode ser passada uma referência às funções assertDispatched
ou assertNotDispatched
para garantir que foi despachado um evento que passa num determinado "teste de veracidade". Se tiver sido despachado pelo menos um evento que passe no teste dado, a declaração é bem-sucedida:
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
Se você gostaria simplesmente de afirmar que um evento está ouvindo em um determinado evento, pode usar o método assertListening
:
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);
ATENÇÃO
Depois de chamar Event::fake()
, nenhum ouvinte de evento será executado. Portanto, se seus testes usam fábricas de modelos que dependem de eventos, como a criação de um UUID durante o evento creating
de um modelo, você deve chamar Event::fake()
após usar suas fábricas.
Fingindo um subconjunto de eventos
Se você quer falsificar apenas um conjunto de eventos específico, poderá passá-los para o método fake
ou fakeFor
:
test('orders can be processed', function () {
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// Outros eventos são despachados normalmente...
$order->update([...]);
});
/**
* Processo de pedido de teste.
*/
public function test_orders_can_be_processed(): void
{
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// Other events are dispatched as normal...
$order->update([...]);
}
É possível falsificar todos os eventos, exceto um conjunto de eventos especificados usando o método except
:
Event::fake()->except([
OrderCreated::class,
]);
Eventos com escopo falsificado
Se você quer apenas simular os eventos de um trecho do teste, pode usar o método fakeFor
:
<?php
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
test('orders can be processed', function () {
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// Os eventos são despachados normalmente e os observadores executarão ...
$order->update([...]);
});
<?php
namespace Tests\Feature;
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* Processo de pedido de teste.
*/
public function test_orders_can_be_processed(): void
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// Os eventos são despachados normalmente e os observadores executarão ...
$order->update([...]);
}
}