Servicio de fábrica de correos electrónicos
Nuestra aplicación envía dos correos electrónicos: en SendBookingRemindersCommand, y enTripController::show(). Aquí hay... mucha duplicación. ¡Me duele la vista! ¡Pero no te preocupes! Podemos reorganizar esto en un servicio de fábrica de correos electrónicos. Y como tenemos pruebas que cubren ambos correos, podemos refactorizar y estar seguros de que no hemos roto nada. No me canso de decirlo: ¡me encantan las pruebas!
BookingEmailFactory
Empieza creando una nueva clase: BookingEmailFactory en el espacio de nombres App\Email. Añade un constructor, copia el argumento $termsPath de TripController::show(), pégalo aquí y conviértelo en una propiedad privada:
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| public function __construct( | |
| #[Autowire('%kernel.project_dir%/assets/terms-of-service.pdf')] | |
| private string $termsPath, | |
| ) { | |
| } | |
| // ... lines 19 - 54 | |
| } |
Ahora, crea dos métodos de fábrica: public function createBookingConfirmation(), que aceptarán Booking $booking, y devolverán TemplatedEmail. Luego,public function createBookingReminder(Booking $booking) también devolverá un TemplatedEmail:
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 19 | |
| public function createBookingConfirmation(Booking $booking): TemplatedEmail | |
| { | |
| // ... lines 22 - 25 | |
| } | |
| // ... line 27 | |
| public function createBookingReminder(Booking $booking): TemplatedEmail | |
| { | |
| // ... lines 30 - 33 | |
| } | |
| // ... lines 35 - 54 | |
| } |
Crea un método para albergar esa maldita duplicación: private function createEmail(), con argumentos Booking $booking y string $tag que devuelve un TemplatedEmail:
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 35 | |
| private function createEmail(Booking $booking, string $tag): TemplatedEmail | |
| { | |
| // ... lines 38 - 53 | |
| } | |
| } |
Salta a TripController::show(), copia todo el código de creación del correo electrónico y pégalo aquí. Arriba, necesitamos dos variables: $customer = $booking->getCustomer() y$trip = $booking->getTrip(). Elimina attachFromPath(), subject(), yhtmlTemplate(). En este TagHeader, utiliza la variable $tag pasada. Podemos dejar los metadatos igual. Por último, devuelve el $email:
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 35 | |
| private function createEmail(Booking $booking, string $tag): TemplatedEmail | |
| { | |
| $customer = $booking->getCustomer(); | |
| $trip = $booking->getTrip(); | |
| $email = (new TemplatedEmail()) | |
| ->to(new Address($customer->getEmail())) | |
| ->context([ | |
| 'customer' => $customer, | |
| 'trip' => $trip, | |
| 'booking' => $booking, | |
| ]) | |
| ; | |
| $email->getHeaders()->add(new TagHeader($tag)); | |
| $email->getHeaders()->add(new MetadataHeader('booking_uid', $booking->getUid())); | |
| $email->getHeaders()->add(new MetadataHeader('customer_uid', $customer->getUid())); | |
| return $email; | |
| } | |
| } |
Con nuestra lógica compartida en su sitio, úsala en createBookingConfirmation(). Escribereturn $this->createEmail(), pasando la variable $booking y booking para la etiqueta. Ahora, ->subject(), copia esto de TripController::show(), cambiando la variable $trippor $booking->getTrip(). Por último, ->htmlTemplate('email/booking_confirmation.html.twig'):
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 19 | |
| public function createBookingConfirmation(Booking $booking): TemplatedEmail | |
| { | |
| return $this->createEmail($booking, 'booking') | |
| ->subject('Booking Confirmation for '.$booking->getTrip()->getName()) | |
| ->htmlTemplate('email/booking_confirmation.html.twig') | |
| ; | |
| } | |
| // ... lines 27 - 54 | |
| } |
Para createBookingReminder(), copia el interior de createBookingConfirmation() y pégalo aquí. Cambia la etiqueta a booking_reminder, el asunto a Booking Reminder, y la plantilla a email/booking_reminder.html.twig:
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 19 | |
| public function createBookingConfirmation(Booking $booking): TemplatedEmail | |
| { | |
| return $this->createEmail($booking, 'booking') | |
| ->subject('Booking Confirmation for '.$booking->getTrip()->getName()) | |
| ->htmlTemplate('email/booking_confirmation.html.twig') | |
| ; | |
| } | |
| // ... lines 27 - 54 | |
| } |
El refactorizador
¡Ahora viene lo divertido! ¡Usar nuestra fábrica y eliminar un montón de código!
En TripController::show(), en lugar de inyectar $termsPath, inyectaBookingEmailFactory $emailFactory:
| // ... lines 1 - 18 | |
| final class TripController extends AbstractController | |
| { | |
| // ... lines 21 - 29 | |
| public function show( | |
| // ... lines 31 - 35 | |
| BookingEmailFactory $emailFactory, | |
| ): Response { | |
| // ... lines 38 - 58 | |
| } | |
| } |
Elimina todo el código de creación de correo electrónico y dentro de $mailer->send(), escribe $emailFactory->createBookingConfirmation($booking):
| // ... lines 1 - 18 | |
| final class TripController extends AbstractController | |
| { | |
| // ... lines 21 - 29 | |
| public function show( | |
| // ... lines 31 - 36 | |
| ): Response { | |
| // ... lines 38 - 39 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 41 - 49 | |
| $mailer->send($emailFactory->createBookingConfirmation($booking)); | |
| // ... lines 51 - 52 | |
| } | |
| // ... lines 54 - 58 | |
| } | |
| } |
En SendBookingRemindersCommand, de nuevo, elimina todo el código de creación de correo electrónico. Arriba en el constructor, autoconecta private BookingEmailFactory $emailFactory:
| // ... lines 1 - 18 | |
| class SendBookingRemindersCommand extends Command | |
| { | |
| public function __construct( | |
| // ... lines 22 - 24 | |
| private BookingEmailFactory $emailFactory, | |
| ) { | |
| // ... line 27 | |
| } | |
| // ... lines 29 - 48 | |
| } |
Aquí abajo, dentro de $this->mailer->send(), escribe $this->emailFactory->createBookingReminder($booking):
| // ... lines 1 - 18 | |
| class SendBookingRemindersCommand extends Command | |
| { | |
| // ... lines 21 - 29 | |
| protected function execute(InputInterface $input, OutputInterface $output): int | |
| { | |
| // ... lines 32 - 37 | |
| foreach ($io->progressIterate($bookings) as $booking) { | |
| $this->mailer->send($this->emailFactory->createBookingReminder($booking)); | |
| // ... line 40 | |
| } | |
| // ... lines 42 - 47 | |
| } | |
| } |
Pruébalo
Oh, sí, ¡qué bien me ha sentado! ¿Pero hemos roto algo? Los canadienses tenemos fama de ser un poco salvajes. Compruébalo ejecutando las pruebas:
bin/phpunit
¡Uh oh, un fallo! Menos mal que tenemos estas pruebas, ¿eh?
El fallo viene de BookingTest:
El mensaje no incluye un archivo con nombre de archivo [Condiciones del servicio.pdf].
Arréglalo
¡Fácil de arreglar! Durante nuestra refactorización, olvidé adjuntar el emocionante PDF de las condiciones del servicio al correo electrónico de confirmación de la reserva. Y nuestros clientes dependen de ello. BuscaBookingEmailFactory::createBookingConfirmation(), y añade->attachFromPath($this->termsPath, 'Terms of Service.pdf'):
| // ... lines 1 - 11 | |
| class BookingEmailFactory | |
| { | |
| // ... lines 14 - 19 | |
| public function createBookingConfirmation(Booking $booking): TemplatedEmail | |
| { | |
| return $this->createEmail($booking, 'booking') | |
| // ... lines 23 - 24 | |
| ->attachFromPath($this->termsPath, 'Terms of Service.pdf') | |
| ; | |
| } | |
| // ... lines 28 - 55 | |
| } |
Vuelve a ejecutar las pruebas:
bin/phpunit
¡Pasadas! ¿Reforzamiento satisfactorio? ¡Comprobado!
A continuación, vamos a cambiar un poco de marcha y sumergirnos en dos nuevos componentes Symfony para consumir los eventos webhook de correo electrónico de Mailtrap.