Często zdarza się, że jedno zdarzenie w naszej aplikacji powoduje konieczność wykonania w kodzie całego szeregu czynności. Piszemy linijkę po linijce kolejne instrukcje i kod szybko staje się długi i trudny w utrzymaniu. Spójrzmy jak możemy wykorzystać mechanizm eventów i listenerów w celu odseparowania konkretnych kawałków kodu, dzięki czemu kod będzie bardziej czytelny i bardziej podatny na zmiany.
Wyobraźmy sobie, że mamy sklep internetowy. Użytkownik przegląda produkty, dodaje je do koszyka po czym przechodzi do realizacji zamówienia. Rejestruje się podając swój adres, wybiera metodę płatności, sposób dostawy i klika przycisk "Złóż zamówienie".
Jest cały szereg czynności, które musi wykonać nasz kod, gdy użytkownik złoży zamówienie. Po pierwsze tworzymy w bazie danych zamówienie na podstawie zawartości koszyka. Potem wysyłamy do użytkownika powiadomienie z potwierdzeniem złożenia zamówienia. Przy okazji wysyłamy maila do administratora strony z informacją o nowym zamówieniu. Następnie zmniejszamy odpowiednio stany magazynowe wszystkich produktów, które zostały zamówione. Obliczamy wagę wszystkich produktów i na tej podstawie zamawiamy kuriera. Na koniec przekierowujemy użytkownika do serwisu płatności.
To oczywiście przykładowe czynności, które będą się różnić w zależności od potrzeb sklepu. Zobaczmy przykładową implementację:
// app/Http/Controllers/CheckoutController.php
<?php
namespace app\Http\Controllers;
use App\Order;
use App\Cart\Cart;
use App\Mail\NewOrder;
use App\Notifications\OrderCreated;
Use App\Shipping\SendIt;
use Illuminate\Support\Facades\Mail;
class CheckoutController extends Controller
{
public function store()
{
$cart = Cart::content();
$order = Order::createFromCart($cart);
// Wyślij użytkownikowi notyfikację z potwierdzeniem złożenia zamówienia
$order->user()->notify(new OrderCreated($order));
// Wyślij administratorowi maila o nowym zamówieniu
Mail::to(config('mail.admin.address'))->send(new NewOrder($order));
// Zmniejsz stany magazynowe produktów
foreach ($order->items as $item) {
$item->product->decreaseStockQuantity($item->quantity);
}
// Oblicz wagę zamówionych produktów i zamów kuriera
$weight = 0;
foreach ($order->items as $item) {
$weight = $weight + $item->product->weight * $item->quantity;
}
$sendIt = new SendIt();
$sendIt->setWeight($weight);
$sendIt->setAddress($order->address);
$shippingOrderNumber = $sendIt->orderShipping();
$order->setShippingOrderNumber($shippingOrderNumber);
Cart::destroy();
return redirect($order->getPaymentMethodRedirectUrl());
}
}
Jak widać, nie jest najlepiej. Robimy wszystko w jednej metodzie kontrolera. Importujemy sporo zależności, fasadę Mail, klasy typu Notification i Mailable. Używamy też, wymyślonej na potrzeby przykładu, klasy SendIt do zamawiania kuriera przez api.
Tylko część kodu odpowiedzialna za stworzenie w bazie danych nowego zamówienia na podstawie zawartości koszyka jest bezpośrednio związana ze złożeniem zamówienia. Reszta to działania poboczne, które aż się proszą, żeby je przenieść gdzieś indziej.
Do rozdzielania tego typu kawałków kodu świetnie nadaje się właśnie mechanizm eventów (zdarzeń) i listenerów (obserwatorów). Event to prosta klasa, która jest swego rodzaju powiadomieniem, że dane zdarzenie miało miejsce w aplikacji. Listener to z kolei klasa, która reaguje na zdarzenie i zawiera kod, który ma się wykonać w związku z wystąpieniem tego zdarzenia.
W naszym przykładzie możemy stworzyć event OrderCreated
:
php artisan make:event OrderCreated
Artisan stworzył nam plik OrderCreated.php
w katalogu app/Events
:
// app/Events/OrderCreated.php
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
W tej klasie ważna dla nas jest jej nazwa OrderCreated
oraz konstruktor, do którego możemy przekazać dane, które będą potrzebne listenerom. Mamy tu zaimportowanych sporo traitów i interfejsów, między innymi do broadcastingu przez WebSockety. To temat na oddzielny wpis, także wyczyśćmy kod z niepotrzebnych w tym momencie importów i metod oraz dodajmy pole $order
wraz z przypisaniem jego wartości przez konstruktor.
// app/Events/OrderCreated.php
<?php
namespace App\Events;
use App\Order;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
class OrderCreated
{
use Dispatchable, SerializesModels;
public $order;
/**
* Create a new event instance.
*
* @param Order $order
*/
public function __construct(Order $order)
{
$this->order = $order;
}
}
Następnie stwórzmy sobie pierwszy listener, który będzie odpowiedzialny za wysłanie użytkownikowi notyfikacji. Użyjemy generatora:
php artisan make:listener SendOrderCreatedNotification
Który utworzył nam taki plik:
// app/Listeners/SendOrderCreatedNotification.php
<?php
namespace App\Listeners;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendOrderCreatedNotification
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
//
}
}
W tej klasie interesuje nas metoda handle
. To w niej musimy umieszczamy kod, który ma się wykonać, gdy puścimy event. Dodajmy kod wysyłana notyfikacji:
<?php
namespace App\Listeners;
use App\Notifications\OrderCreated;
class SendOrderCreatedNotification
{
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$event->order->user()->notify(new OrderCreated($event->order));
}
}
Pod zmienną $event
w metodzie handle
mamy dostęp do instancji klasy \App\Events\OrderCreated
, w której z kolei przypisaliśmy pole $order
z naszym zamówieniem. Dlatego możemy się do tego zamówienia odwołać przez $event->order
.
W tej chwili mamy event i listener. Musimy jeszcze poinformować naszą aplikację, że te dwie klasy mają być ze sobą powiązane. To znaczy, że dany listener ma reagować na puszczenie danego eventu. Jak z wszystkim innym w Laravelu, można to zrobić na kilka sposobów, ale najłatwiej jest dodać odpowiedni wpis w pole $listen
w klasie EventServiceProvider
:
// app/Providers/EventServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
\App\Events\OrderCreated::class => [
\App\Listeners\SendOrderCreatedNotification::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();
//
}
}
Wróćmy teraz do naszego kontrolera i usuńmy linijkę wysyłająca powiadomienie do użytkownika. Następnie puśćmy event OrderCreated
używając funkcji globalnej event()
:
// app/Http/Controllers/CheckoutController.php
<?php
namespace App\Http\Controllers;
use App\Cart\Cart;
use App\Events\OrderCreated;
use App\Mail\NewOrder;
Use App\Shipping\SendIt;
use Illuminate\Support\Facades\Mail;
class CheckoutController extends Controller
{
public function index()
{
$cart = Cart::content();
return view('cart.checkout')->with('cart', $cart);
}
public function store()
{
$cart = Cart::content();
$order = Order::createFromCart($cart);
event(new OrderCreated($order));
// Wyślij administratorowi maila o nowym zamówieniu
Mail::to(config('mail.admin.address'))->send(new NewOrder($order));
// Zmniejsz stany magazynowe produktów
foreach ($order->items as $item) {
$item->product->decreaseStockQuantity($item->quantity);
}
// Zamów kuriera
$sendIt = new SendIt();
$weight = 0;
foreach ($order->items as $item) {
$weight = $weight + $item->product->weight * $item->quantity;
}
$sendIt->setWeight($weight);
$sendIt->setAddress($order->address);
$shippingOrderNumber = $sendIt->orderShipping();
$order->setShippingOrderNumber($shippingOrderNumber);
Cart::destroy();
return redirect($order->getPaymentMethodRedirectUrl);
}
}
Interesuje nas ta linijka:
event(new OrderCreated($order));
Puściliśmy w ten sposób zdarzenie OrderCreated
. Poinformowaliśmy naszą aplikację, że zamówienie zostało złożone. Teraz Laravel sprawdzi jakie listenery powinny reagować na to zdarzenie na podstawie tablicy w polu $listen
w pliku app\Providers\EventServiceProvider
i wykona kod w nich zawarty.
Przeróbmy kolejne czynności na klasy typu Listener, zaczynając od wysłania maila do administratora:
php artisan make:listener SendNewOrderMail
// app/Listeners/SendNewOrderMail.php
<?php
namespace App\Listeners;
use App\Mail\NewOrder;
use Illuminate\Support\Facades\Mail;
class SendNewOrderMail
{
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
Mail::to(config('mail.admin.address'))->send(new NewOrder($event->order));
}
}
Następnie zmniejszenie stanów magazynowych:
php artisan make:listener ReduceProductStockQuantities
// app/Listeners/ReduceProductStockQuantities.php
<?php
namespace App\Listeners;
class ReduceProductStockQuantities
{
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
foreach ($event->order->items as $item) {
$item->product->decreaseStockQuantity($item->quantity);
}
}
}
I zamówienie kuriera:
php artisan make:listener PlaceShippingOrder
// app/Listeners/PlaceShippingOrder.php
<?php
namespace App\Listeners;
Use App\Shipping\SendIt;
class PlaceShippingOrder
{
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$sendIt = new SendIt();
$weight = 0;
foreach ($event->order->items as $item) {
$weight = $weight + $item->product->weight * $item->quantity;
}
$sendIt->setWeight($weight);
$sendIt->setAddress($event->order->address);
$shippingOrderNumber = $sendIt->orderShipping();
$event->order->setShippingOrderNumber($shippingOrderNumber);
}
}
Zarejestrujmy te nowo powstałe klasy w polu $listen
w pliku app/Providers/EventServiceProvider
:
protected $listen = [
\App\Events\OrderCreated::class => [
\App\Listeners\SendOrderCreatedNotification::class,
\App\Listeners\SendNewOrderMail::class,
\App\Listeners\ReduceProductStockQuantities::class,
\App\Listeners\PlaceShippingOrder::class,
],
];
I usuńmy ich kod z naszego kontrolera:
<?php
namespace App\Http\Controllers;
use App\Cart\Cart;
use App\Events\OrderCreated;
class CheckoutController extends Controller
{
public function store()
{
$cart = Cart::content();
$order = Order::createFromCart($cart);
event(new OrderCreated($order));
Cart::destroy();
return redirect($order->getPaymentMethodRedirectUrl);
}
}
Jak widać, kod naszego kontrolera zdecydowanie się uprościł. Tworzymy zamówienie na podstawie zawartości koszyka, następnie informujemy naszą aplikację, że miało miejsce zdarzenie OrderCreated
. Kod który ma się w związku z tym wykonać jest umieszczony już gdzieś indziej. Jeśli będziemy chcieli dodać kolejną czynność, która powinna się wykonać, nie musimy dotykać istniejących klas, a jedynie dodać nowy listener i podpiąć go pod zdarzenie OrderCreated
.