Email Twig Layout
Lucky you! You found an early release chapter - it will be fully polished and published shortly!
This Chapter isn't quite ready...
Rest assured, the gnomes are hard at work
completing this video!
New feature time! I want to send a reminder email to customers 1 week before their booked trip. T minus 1 week to lift off people!
Symfony CLI Worker Issue
First though, we have a little problem with our Symfony CLI worker. Open
.symfony.local.yaml
. Our messenger
worker is watching the vendor
directory for changes. At least on some systems, there's just too many
files in here to monitor and some weird things happen. No big deal:
remove vendor/
:
workers: | |
// ... lines 2 - 5 | |
messenger: | |
// ... line 7 | |
watch: ['config', 'src', 'templates'] |
And since we changed the config, jump to your terminal and restart the webserver:
symfony server:stop
And:
symfony serve -d
Email Layout
Our new booking reminder email will have a template very similar to the booking
confirmation one. To reduce duplication, and keep our snazzy emails consistent,
in templates/email/
, create a new layout.html.twig
template that all
our emails will extend.
Copy the contents of booking_confirmation.html.twig
and paste here. Now, remove
the booking-confirmation-specific content and create an empty content
block. I think
it's fine to keep our signature here.
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css'), source('@styles/email.css')) %} | |
<container> | |
{% block content %}{% endblock %} | |
<row> | |
<columns> | |
<p>We can't wait to see you there,</p> | |
<p>Your friends at Universal Travel</p> | |
</columns> | |
</row> | |
</container> | |
{% endapply %} |
In booking_confirmation.html.twig
, up top here, extend this new layout and add the
content
block. Down below, copy the email-specific content and paste it inside that
block. Remove everything else.
{% extends 'email/layout.html.twig' %} | |
{% block content %} | |
<row> | |
<columns> | |
<spacer size="40"></spacer> | |
<p class="accent-title">Get Ready for your trip to</p> | |
<h1 class="trip-name">{{ trip.name }}</h1> | |
<img | |
class="trip-image float-center" | |
src="{{ email.image('@images/%s.png'|format(trip.slug)) }}" | |
alt="{{ trip.name }}"> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<p class="accent-title">Departure: {{ booking.date|date('Y-m-d') }}</p> | |
</columns> | |
</row> | |
<row> | |
<columns> | |
<button class="expanded rounded center" href="{{ url('booking_show', {uid: booking.uid}) }}"> | |
Manage Booking | |
</button> | |
<button class="expanded rounded center secondary" href="{{ url('bookings', {uid: customer.uid}) }}"> | |
My Account | |
</button> | |
</columns> | |
</row> | |
{% endblock %} |
Let's make sure the booking confirmation email still works - and we have tests for that! Back in the terminal, run them with:
bin/phpunit
Green! That's a good sign. Let's be doubly sure by checking it in Mailtrap. In the app, book a trip... and check Mailtrap. I still looks fantastic!
Time to bang out the reminder email!
Booking Reminder Flag
After an email reminder is sent, we need to mark the booking so
that we don't annoy the customer with multiple reminders. Let's add a new flag for
this to the Booking
entity.
In your terminal, run:
symfony make:entity Booking
Oops!
symfony console make:entity Booking
Add a new field called reminderSentAt
, type datetime_immutable
, nullable? Yes.
This is a common pattern I use for these type of flag fields instead of a simple boolean
.
null
means false
and a date means true
. It works the same, but gives us a bit more
info.
Hit enter to exit the command.
In the Booking
entity... here's our new property, and down here, the getter and setter.
Finding Bookings to Remind
Next, we need a way to find all bookings that need a reminder sent. Perfect job for
BookingRepository
! Add a new method called findBookingsToRemind()
, return type: array
.
Add a docblock to show it returns an array of Booking objects:
// ... lines 1 - 12 | |
class BookingRepository extends ServiceEntityRepository | |
{ | |
// ... lines 15 - 51 | |
/** | |
* @return Booking[] | |
*/ | |
public function findBookingsToRemind(): array | |
{ | |
// ... lines 57 - 65 | |
} | |
} |
Inside, return $this->createQueryBuilder()
, alias b
. Chain
->andWhere('b.reminderSentAt IS NULL')
, ->andWhere('b.date <= :future')
,
->andWhere('b.date > :now')
filling in the placeholders with
->setParameter('future', new \DateTimeImmutable('+7 days'))
and
->setParameter('now', new \DateTimeImmutable('now'))
. Finish with ->getQuery()->getResult()
:
// ... lines 1 - 12 | |
class BookingRepository extends ServiceEntityRepository | |
{ | |
// ... lines 15 - 54 | |
public function findBookingsToRemind(): array | |
{ | |
return $this->createQueryBuilder('b') | |
->andWhere('b.reminderSentAt IS NULL') | |
->andWhere('b.date <= :future') | |
->andWhere('b.date > :now') | |
->setParameter('future', new \DateTimeImmutable('+7 days')) | |
->setParameter('now', new \DateTimeImmutable('now')) | |
->getQuery() | |
->getResult() | |
; | |
} | |
} |
Pending Reminder Booking Fixture
In AppFixtures
, down here, we create some
fake bookings. Add one that will for sure trigger a reminder email to be sent:
BookingFactory::createOne()
, inside, 'trip' => $arrakis, 'customer' => $clark
and,
this is the important part, 'date' => new \DateTimeImmutable('+6 days')
:
// ... lines 1 - 10 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 15 - 87 | |
BookingFactory::createOne([ | |
'trip' => $arrakis, | |
'customer' => $clark, | |
'date' => new \DateTimeImmutable('+6 days'), | |
]); | |
} | |
} |
Clearly between now and 7 days from now.
"Migration"
We made changes to the structure of our database. Normally, we should be creating a migration... but, we aren't using migrations. So, we'll just force update the schema. In your terminal, run:
symfony console doctrine:schema:update --force
Then, reload the fixtures:
symfony console doctrine:fixture:load
That all worked, great!
Next, we'll create a new reminder email and a CLI command to send them!