If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSymfony has had Docker support for a while, in particular, to help with local web development. For example, I have PHP installed locally. So I'm not using Docker to get PHP itself. But my project has a docker-compose.yml
file that defines a database service. Remember that the local web server we're using comes from the Symfony binary... and it's smart. It automatically detects that I have docker-compose
running with a database
service... and so it reads the connection parameters from this container and exposes them as a DATABASE_URL
environment variable.
Check this out! On any page, click into the web debug toolbar. Make sure you're on "Request/ Response", then go to "Server Parameters". Scroll down to find DATABASE_URL
set to (in my case) 127.0.0.1
on port 56239
. The way my docker-compose.yml
is set up, it will create a new random port each time it starts.
... line 1 | |
services: | |
database: | |
image: 'mysql:8.0' | |
environment: | |
MYSQL_ROOT_PASSWORD: password | |
ports: | |
# To allow the host machine to access the ports below, modify the lines below. | |
# For example, to allow the host to connect to port 3306 on the container, you would change | |
# "3306" to "3306:3306". Where the first port is exposed to the host and the second is the container port. | |
# See https://docs.docker.com/compose/compose-file/#ports for more information. | |
- '3306' |
The Symfony binary will then figure out which random port it is and create the environment variable accordingly. Finally, just like normal, thanks to our config/packages/doctrine.yaml
configuration, the DATABASE_URL
environment variable is used to talk to the database. So the Symfony binary plus Docker is a nice way to quickly and easily boot up external services like a database, elastic search, or more.
Recently, Symfony took this to the next level. On Symfony.com, you'll find a blog post called Introducing Docker support. The idea is pretty simple. When you install a new package - Doctrine, for example - that package's recipe may ship with some Docker configuration. And so, just by installing the package, you get Docker configuration automatically.
Let's see this in action! Since we already have Doctrine installed, let's install Mailer, which will come with docker-compose
config for a service called MailCatcher. At your terminal, run:
composer require mailer
Awesome! It stops us and asks:
The recipe for this package contains some Docker configuration. Do you want to include Docker configuration from recipes?
I'm going to say p
for "Yes permanently". If you don't want the Docker stuff, no worries! Answer no or "No permanently" and it will never ask you again.
And... done! Now we can run
git status
to see that it updated the normal stuff, but also gave us a new docker-compose.override.yml
. If you're not familiar, Docker will first read docker-compose.yml
and then will read docker-compose.override.yml
. The purpose of the override file is to change configuration that is specific to your machine. In this case, our local machine.
... lines 1 - 2 | |
services: | |
###> symfony/mailer ### | |
mailer: | |
image: schickling/mailcatcher | |
ports: [1025, 1080] | |
###< symfony/mailer ### |
The new file adds a service called mailer
... which boots up something called MailCatcher. MailCatcher is a local debugging tool that starts an SMTP server that you can send emails to. And then it gives you a web GUI where you can review those emails... inside a pretend inbox.
This service lives inside of docker-compose.override.yml
because we only want this service to be running locally when we're doing local development. If you're using Docker to deploy your site, you'll have a different local configuration for production. If you're not deploying with Docker, all of this config could live in your main docker-compose.yml
file if you want.
Anyways, before we even start using this service, let's get set up to send an email. Open up src/Controller/RegistrationController.php
. We're already using symfonycasts/verify-email-bundle
... but instead of actually sending the verification email, we're just putting the verification URL directly into a flash message. It was a shortcut I made during the Security tutorial.
... lines 1 - 16 | |
class RegistrationController extends AbstractController | |
{ | |
... line 19 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper, EntityManagerInterface $entityManager): Response | |
{ | |
... lines 22 - 24 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 26 - 43 | |
// TODO: in a real app, send this as an email! | |
$signedUrl = $signatureComponents->getSignedUrl(); | |
$this->addFlash('success', sprintf( | |
'Confirm your email at: %s', | |
$signedUrl | |
)); | |
... lines 50 - 51 | |
} | |
... lines 53 - 56 | |
} | |
... lines 58 - 88 | |
} |
But now, let's send a real email. I'll go to the bottom of the class and paste a new private function, which you can get from the code blocks on this page. Retype the "e" on MailerInterface
and hit "tab" to add that use
statement... and do the same with the "l" on Email
. Select the one from Symfony\Component\Mime
.
... lines 1 - 8 | |
use Symfony\Component\Mailer\MailerInterface; | |
use Symfony\Component\Mime\Email; | |
... lines 11 - 19 | |
class RegistrationController extends AbstractController | |
{ | |
... lines 22 - 92 | |
private function sendVerificationEmail(MailerInterface $mailer, User $user, string $signedUrl) | |
{ | |
$email = (new Email()) | |
->from('hello@example.com') | |
->to($user->getEmail()) | |
//->cc('cc@example.com') | |
//->bcc('bcc@example.com') | |
//->replyTo('fabien@example.com') | |
//->priority(Email::PRIORITY_HIGH) | |
->subject('Verify your email on Cauldron Overflow!') | |
->text('Please, follow the link to verify your email!') | |
->html(sprintf('<a href="%s">%s</a>', $signedUrl, $signedUrl)); | |
$mailer->send($email); | |
} | |
} |
Perfect! This will send a very simple verification email that just contains the verification link.
Now, all the way up on the register()
method, add a new argument at the end: MailerInterface $mailer
. Then, down here, remove the TODO
... and replace it with $this->sendVerificationEmail()
passing $mailer
, $user
, and $signedUrl
. Finally, in the success
flash, change the message to tell the user that they should check their email.
... lines 1 - 22 | |
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper, EntityManagerInterface $entityManager, MailerInterface $mailer): Response | |
{ | |
... lines 25 - 27 | |
if ($form->isSubmitted() && $form->isValid()) { | |
... lines 29 - 47 | |
$this->sendVerificationEmail($mailer, $user, $signedUrl); | |
$this->addFlash('success', sprintf( | |
'Confirm your email - the verify link was sent to %s', | |
$user->getEmail() | |
)); | |
... lines 53 - 54 | |
} | |
... lines 56 - 59 | |
} | |
... lines 61 - 109 |
Okay, so we have this new docker-compose.override.yml
file with MailCatcher. However, that container isn't actually running yet. But, ignore that for a minute... and let's see if we can get the email working.
Click back to the Register page... whoops! We get an error:
Environment variable not found: "MAILER_DSN".
Of course! The mailer service needs this environment variable to tell it where to send emails. You can find this inside .env
: the mailer recipe gave us the MAILER_DSN
env var, but it's commented-out. Un-comment that.
... lines 1 - 30 | |
###> symfony/mailer ### | |
MAILER_DSN=null://null | |
### |
By default, it sends emails to what's called the "null transport"... which means that when we send emails... they go absolutely nowhere. They're not actually delivered... which is a nice setting for development.
Refresh, add a fake email address, register, and... it worked! Of course, it didn't send the email anywhere... but we can still see, more or less, what the email would look like.
How? Click any link to go into the Profiler, click "Last 10", find the POST request for /register
and click into that. Down here, go to the "E-mails" section and... voilà! It shows our email including an HTML preview. And wow is it ugly... but that's my fault. Btw, the HTML preview is a new feature in Symfony 5.4.
Ok that's cool. But let's see how MailCatcher can also help us debug emails. First, if you do not already have a docker-compose.yml
file, create one. All you need is the version
line on top. That way we have a docker-compose.yml
file and a docker-compose.override.yml
file.
Now, find your terminal and run:
docker-compose up -d
I already have docker-compose
running for my database container, but this will now start the mailer
container, which will initialize a new mailcatcher SMTP server.
Ok... so how do we configure mailer
to deliver to this smpt server from MailCatcher? What port is that SMTP server running on anyways? The answer is... we don't know! And we don't care.
Watch this. Go back to any page, refresh... and then click into the Profiler. Once again, make sure you're on the "Request/Response" section then go to "Server Parameters". Scroll down to MAILER_URL
.
Woh! MAILER_URL
is suddenly set to smtp://127.0.0.1:65320
!
Here's what happened. When we started the mailer
service, Docker exposed port 1025
of that container - which is the SMTP server - to a random port on my host machine. The Symfony binary saw that, read the random port, and then, just like with the database, exposed a MAILER_URL
environment variable that points to it. In other words, our emails will already send to MailCatcher!
Let's try it! I'll sign up again with some other email address, agree to the terms and... cool! No error! To see the email, we could go back into the Profiler like we did a minute ago. But in theory, if that sent to MailCatcher, we should be able to go to the MailCatcher UI and review the message there. The question is, where is the MailCatcher
UI? What port is that running on? Because that's also running on a random port.
To help with this, hover over the "Server" section of the web debug toolbar. You can see that it detects that docker-compose
is running, it is exposing some environment variables from Docker, and it even detected Webmail! Click "Open" to head into MailCatcher... and there's our email!
If you send more emails, they'll show up here like a little inbox.
And... that's it! Congrats! You've just upgraded your app to Symfony 6! And PHP 8! And PHP attributes! Such cool stuff!
If you have any questions or run into any problems during your upgrade that we didn't talk about, we're here for you down in the comments. All right, friends, seeya next time!
Hey Fabrice
Thanks for telling us what you'd like to learn next. I can't guarantee anything but we take into account all the requests
Cheers!
Hi there! Great video. But I am having a small problem with Chapter 19 ... I'm using the latest full symfony-docker from https://github.com/dunglas/symfony-docker. I have successfully added symfony/mailer package and can see the mailcatcher docker running. But the MAILER_* server variables are just not there... Is this simply not fit for the symfony-docker? or did symfony 6.1 break/change something? already tried clearing caches and rebuilding everything...
Hey SaronGrave,
Did you update your docker-compose.yaml
config file after installing Mailer?
Oh, and how are you running your web server? Remember it has to be executed through the Symfony CLI so it can inject the env vars symfony server:run
Cheers!
I've completed the course and the final challenge however the course shows as 92% complete. Any idea what I've missed?
Cheers
Hey Steve,
Please, take a look at the table of content on this page: https://symfonycasts.com/sc... - please make sure you have a "check" icon for every chapter in that list. If anything is missing - you would need to watch that video till the end to get a credit for it and mark it as viewed with the "check" icon. Also, make sure that all challenges in that list are green, if you missed any or have some red there - try to complete them again.
If you still have this problem and don't know - please, contact us via our contact form: https://symfonycasts.com/co... - so we know your email and could take a look at your account :)
Cheers!
HI Victor
I actual had the first video unticked... however I'm still not 100%. The only thing I can see now is the final challenge is not green despite having done it correctly several times. I'll drop a message in via the contact form. Thank you
Hey Steve,
No problem, I noticed your email and already replied to you :) Now you have that cert ;)
Cheers!
// composer.json
{
"require": {
"php": "^8.0.2",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.6", // v3.6.1
"composer/package-versions-deprecated": "^1.11", // 1.11.99.5
"doctrine/annotations": "^1.13", // 1.13.2
"doctrine/dbal": "^3.3", // 3.3.5
"doctrine/doctrine-bundle": "^2.0", // 2.6.2
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.0", // 2.11.2
"knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
"knplabs/knp-time-bundle": "^1.18", // v1.18.0
"pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
"pagerfanta/twig": "^3.6", // v3.6.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.6
"sentry/sentry-symfony": "^4.0", // 4.2.8
"stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.7
"symfony/console": "6.0.*", // v6.0.7
"symfony/dotenv": "6.0.*", // v6.0.5
"symfony/flex": "^2.1", // v2.1.7
"symfony/form": "6.0.*", // v6.0.7
"symfony/framework-bundle": "6.0.*", // v6.0.7
"symfony/mailer": "6.0.*", // v6.0.5
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/property-access": "6.0.*", // v6.0.7
"symfony/property-info": "6.0.*", // v6.0.7
"symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
"symfony/routing": "6.0.*", // v6.0.5
"symfony/runtime": "6.0.*", // v6.0.7
"symfony/security-bundle": "6.0.*", // v6.0.5
"symfony/serializer": "6.0.*", // v6.0.7
"symfony/stopwatch": "6.0.*", // v6.0.5
"symfony/twig-bundle": "6.0.*", // v6.0.3
"symfony/ux-chartjs": "^2.0", // v2.1.0
"symfony/validator": "6.0.*", // v6.0.7
"symfony/webpack-encore-bundle": "^1.7", // v1.14.0
"symfony/yaml": "6.0.*", // v6.0.3
"symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.8
"twig/string-extra": "^3.3", // v3.3.5
"twig/twig": "^2.12|^3.0" // v3.3.10
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
"phpunit/phpunit": "^9.5", // 9.5.20
"rector/rector": "^0.12.17", // 0.12.20
"symfony/debug-bundle": "6.0.*", // v6.0.3
"symfony/maker-bundle": "^1.15", // v1.38.0
"symfony/var-dumper": "6.0.*", // v6.0.6
"symfony/web-profiler-bundle": "6.0.*", // v6.0.6
"zenstruck/foundry": "^1.16" // v1.18.0
}
}
Hey, you talked about ElasticSearch in this video, why not make a course for ElasticSearch ? it could be awesome!