Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

12
Login or Register to join the conversation
Default user avatar
Default user avatar Marco | posted 1 year ago | edited

Hello there ,
I not understand this part, "That's it! Symfony will detect the domain name". What if I'm sending email using command line? Symfony will not detect the domain name, how to configure that in my .env.local? I think, maybe this part is missing in this article. For all the rest, excellent article! Regards.

Reply

Hey Marco!

Excellent question! We tackle this a bit later in the tutorial: https://symfonycasts.com/sc...

And also, the config needed for this has been slightly simplified: https://symfony.com/blog/ne... - you can use our approach in the tutorial (it works fine!) or adapt to this new config.

Cheers!

Reply
Daniel W. Avatar
Daniel W. Avatar Daniel W. | posted 2 years ago

Hi,
some images like the logo in the email and the Article images are not loaded and I don't understand why.
All other images work.
I ran:
composer install
yarn install
yarn build

I'm using webpack encore for my other projects too, so I don't think it's related to my yarn/npm installation.

This is how the html looks like in the email:
<img src="https://127.0.0.1:8000/build/images/email.logo.png" class="logo" alt="SpaceBar Logo">

The exception is : No route found for "GET /build/images/email.logo.png"

My public folder contains the image tho I checked it : public/build/images/email/logo.66125a81.png

edit: the email logo was my fault a typo but the articles are still broken mybe its the twigextention/function?

Reply

Hey @Ecludio!

Hmm. Well first, good job debugging so far :).

For the article images that aren’t working, what does the final HTML look like? And what error do you get if you try those URLs directly on your browser?

Btw, there is some special logic to get these files set up correctly. When you load the fixtures, the ArticleFixtures.php file (in src/DataFixtures) does a, sort of, “fake” upload where it copies the image files into the public/. I’m not sure if this will help, but I wanted to highlight it.

Anyways, let me know what you find out!

Cheers!

Reply

Hi,

just to warn you that chapter 5 is not public, but chapter 6 is.
Thank you again for your work !

Reply

Ok that seems to have been fixed

Reply

Hey John,

Hm, most probably the problem was in a different thing, maybe temporary video outage... depends on the error you saw. Anyway, glad to hear it works for you now.

Cheers!

Reply
AymDev Avatar
AymDev Avatar AymDev | posted 3 years ago | edited

Isn't Symfony calling strip_tags() instead of strip_slashes() ?

Reply

Hey AymDev,

Ah, you're right! We meant "strip_tags()" but said "strip_slashes()". I fixed it in https://github.com/knpunive...

Thank you for reporting it!

Cheers!

Reply
Ernest H. Avatar
Ernest H. Avatar Ernest H. | posted 3 years ago

Love these twig tips. What is the best method to retrieve the root directory from within my command class? I could use DI to inject the KernelInterface and use `$this->kernel->getRootDir()`, or I can alternatively inject ParameterBagInterface and use `$this->parameterInterface->get('kernel.project_dir')`. I'm guessing these are pretty equivalent, but I don't know if there is a preferred or better way??

Reply

Hey Ernest H.

Yes there is another way, and we think it's best way to do such things. You can autowire using DI, in services.yaml under bind section you can define something like project_dir: '%kernel.project_dir%' and then feel free to use it in any constructor like:


public function __construct(string $project_dir) {
    $this->projectDir = $project_dir;
}

Cheers!

Reply
Ernest H. Avatar

thanks!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "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.9", // v1.17.6
        "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
    }
}
userVoice