Absolute URLs to Routes & Assets

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

The HTML content of our email will use this template... which is still totally static. For example, see this link going to #homepage? That's just a placeholder. Normally in a template, we would use the {{ path() }} function to generate a URL to the homepage route. The name of that route is... check out ArticleController... there it is: the homepage route name is app_homepage. So we would normally say path('app_homepage').

Using the url() Function

The problem is that this will generate a relative URL - it will literally generate href="/". But for an email, all paths must be absolute. To force that, change path() to url().

<!doctype html>
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
<div class="header text-center">
<a href="{{ url('app_homepage') }}">
... line 61
</a>
</div>
... lines 64 - 93
</div>
</div>
</body>
</html>

That's it! Symfony will detect the domain name - localhost:8000 while we're coding locally - and use that to prefix the URL.

Let's fix a few other URLs: for the link to create a new article, replace the hardcoded string with url() and the name of that route, which if you looked in the app, is admin_article_new.

<!doctype html>
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
... lines 59 - 63
<div class="content">
... lines 65 - 69
<p class="block text-center">
<a href="{{ url('admin_article_new') }}" class="btn">Get writing!</a>
</p>
... lines 73 - 83
</div>
... lines 85 - 93
</div>
</div>
</body>
</html>

At the bottom, there's one more link to the homepage. Say {{ url('app_homepage') }}.

<!doctype html>
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
... lines 59 - 63
<div class="content">
... lines 65 - 75
<p class="block text-center">
<a href="{{ url('app_homepage') }}" class="btn">Get reading!</a>
</p>
... lines 79 - 83
</div>
... lines 85 - 93
</div>
</div>
</body>
</html>

A Bit about Webpack Encore & Images

Links, done! But there's one other path we need to fix: the path to this image. But... forget about emails for a minute. This project uses Webpack Encore to compile its assets: I have an assets/ directory at the root, an images directory inside that, and an email/logo.png file that I want to reference. You don't need to run Encore, but if you did, I've configured it to copy that file into a public/build/images/ directory. There it is: public/build/images/email/logo.66125a81.png.

If you downloaded the starting code for the tutorial, you don't need to worry about running Encore... only because we ran it for you and included the final, built public/build directory. I mean, you can run Encore if you want - you just don't need to because the built files are already there.

The point is, whether you're using Encore or not, the end goal is to generate an absolute URL to a file that lives somewhere in your public/ directory. To do that in Twig, we use the {{ asset() }} function. Pass this build/images/email/logo.png. Because we're using Encore, we don't need to include the version hash that's part of the real file: the asset function will add that automatically. Go team!

If you're not using Encore, it's the same process: just use asset() then include the actual path to the physical file, relative to the public/ directory.

Absolute Image Paths

But... this leaves us with the same problem we had for the generated URLs! By default, the asset() function generates relative URLs: they don't contain the domain name. To fix that, wrap this in another function: absolute_url().

<!doctype html>
<html lang="en">
... lines 3 - 55
<body>
<div class="body">
<div class="container">
<div class="header text-center">
<a href="{{ url('app_homepage') }}">
<img src="{{ absolute_url(asset('build/images/email/logo.png')) }}" class="logo" alt="SpaceBar Logo">
</a>
</div>
... lines 64 - 93
</div>
</div>
</body>
</html>

And... done! Ready to try this? Move over to the site, go back, change the email address again... we're going to do this a lot... type a new password, wave a magic wand and... hit enter. Ok... no errors... a good sign!

Over in Mailtrap, it's already there! Oh, it looks so much better: we even have a working image and, if we hover over a link, the URL does contain our domain: localhost:8000. This is even more obvious in the HTML source: everything has a full URL.

Automatic "Text" Part

Woh, and... our email also has a text part! How did that happen? In the controller, we only called htmlTemplate() - we removed our call to the text() method. Well... thank you Mailer. If you set the HTML on an email but do not explicitly set the text, Symfony automatically adds it for you by calling strip_tags() on your HTML. That's awesome.

Well... awesome... but not totally perfect: it included all the styles on top! Don't worry: we'll fix that soon... kinda on accident. But the bottom looks pretty great... with zero effort.

Next, the URLs and image paths in our email are now dynamic... but nothing else is! Any self-respecting email must have real data, like the name of the user... or their favorite color. Let's make the email truly dynamic by passing in variables. We'll also find out what other information is available for free from inside an email template.

Leave a comment!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4
    }
}