Router Request Context: Fix Paths in the CLI

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We sent the email, but it's missing its core content: info about the articles that each author wrote last week. That's no problem for us: we're already passing an articles variable to the template via context(). In the template, replace the <tr> with {% for article in articles %}:

... lines 1 - 2
{% block content %}
... lines 4 - 12
<row>
<columns>
<table>
<tr>
<th>#</th>
<th>Title</th>
<th>Comments</th>
</tr>
{% for article in articles %}
... lines 22 - 26
{% endfor %}
</table>
</columns>
</row>
... lines 31 - 39
{% endblock %}

add the <tr>, a <td> and print some data: {{ loop.index }} to number the list, 1, 2, 3, 4, etc, {{ article.title }} and finally, how about: {{ article.comments|length }}.

... lines 1 - 2
{% block content %}
... lines 4 - 12
<row>
<columns>
<table>
<tr>
<th>#</th>
<th>Title</th>
<th>Comments</th>
</tr>
{% for article in articles %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ article.title }}</td>
<td>{{ article.comments|length }}</td>
</tr>
{% endfor %}
</table>
</columns>
</row>
... lines 31 - 39
{% endblock %}

That's good enough. Double check that by running the command:

php bin/console app:author-weekly-report:send

And... in Mailtrap... we are good.

Now let's turn to the glaring, horrible bug in our email! Ah! As I mentioned a few minutes ago, if you hover over the link its, gasp, broken! For some reason, it points to localhost not our real domain... which is localhost:8000. Close, but not right.

Hmm. In the template... yea... that looks right: {{ url('app_homepage') }}. Ok, then why - when we click on the link - is it broken?

... lines 1 - 2
{% block content %}
... lines 4 - 30
<row>
<columns>
<center>
... line 34
<button href="{{ url('app_homepage') }}">Check on the Space Bar</button>
... line 36
</center>
</columns>
</row>
{% endblock %}

We know that the url() function tells Symfony to generate an absolute URL. And... it is. I'll run "Inspect Element" on the broken link button. Check out the href: http://localhost not localhost:8000. The same thing would happen if you deployed this to production: it would always say localhost. The URL is absolute... it's just wrong!

Why? Think about it: in the registration email - where this did work - how did Symfony know what our domain was when it generated the link? Did we configure that somewhere? Nope! When you submit the registration form, Symfony simply looks at what the current domain is - localhost:8000 - and uses that for all absolute URLs.

But when you're in a console command, there is no request! Symfony has no idea if the code behind this site is deployed to localhost:8000, example.com, or lolcats.com. So, it just guesses localhost... which is totally wrong... but probably better than guessing lolcats.com?

If you're sending emails from the command line - or rendering templates for any reason that contain paths - you need to help Symfony: you need to tell it what domain to use.

Setting router.request_context

To fix this, start by looking inside our .env file. One of our keys here is called SITE_BASE_URL.

39 lines .env
... lines 1 - 32
SITE_BASE_URL=https://localhost:8000
... lines 36 - 39

It is the URL to our app. But, but, but! This is not a standard Symfony environment variable and Symfony is not currently using this. Nope, this is an environment variable that we invented in our file uploads tutorial for a totally different purpose. You can see it used in config/services.yaml. It has nothing to do with how Symfony generates URLs.

Anyways, to fix the path problem, you need to set two special parameters. The first is router.request_context.scheme, which you'll set to https or http. The other is router.request_context.host which, for our local development, will be localhost:8000.

Now obviously, we don't want to hardcode these - at least not the second value: it will be different on production. Instead, we need to set these as new environment variables. And... hey! In .env, the SITE_BASE_URL is almost what we need... we just need it to be kind of split into two pieces. Hmm.

Check this out, create two new environment variables: SITE_BASE_SCHEME set to https and SITE_BASE_HOST set to localhost:8000.

41 lines .env
... lines 1 - 31
### END CUSTOM VARS
SITE_BASE_SCHEME=https
SITE_BASE_HOST=localhost:8000
... lines 36 - 41

Back in services.yaml, use these values: %env(SITE_BASE_SCHEME)% and %env(SITE_BASE_HOST)%

... lines 1 - 5
parameters:
... lines 7 - 9
router.request_context.scheme: '%env(SITE_BASE_SCHEME)%'
router.request_context.host: '%env(SITE_BASE_HOST)%'
... lines 12 - 53

Cool!

Using Environment Variables... in Environment Variables

The problem is that we now have some duplication. Fortunately, one of the properties of environment variables is that... um... they can contain environment variables! For SITE_BASE_URL, set it to $SITE_BASE_SCHEME - yep, that's legal - :// and then $SITE_BASE_HOST.

41 lines .env
... lines 1 - 31
### END CUSTOM VARS
SITE_BASE_SCHEME=https
SITE_BASE_HOST=localhost:8000
SITE_BASE_URL=$SITE_BASE_SCHEME://$SITE_BASE_HOST
... lines 37 - 41

I love that trick. Anyways, now that we've set those two parameters, Symfony will use them to generate the URL instead of trying to guess it.

Tip

This works, but if you need to override the scheme or host in .env.local, you would also need to repeat the SITE_BASE_URL= to set it again. A better solution would be to set the SITE_BASE_URL just once using a config trick in services.yaml:

parameters:
    env(SITE_BASE_URL): '%env(SITE_BASE_SCHEME)%://%env(SITE_BASE_HOST)%'

Try the command one last time:

php bin/console app:author-weekly-report:send

And... check it out in Mailtrap! Yes! This time the link points to localhost:8000.

Next! Let's talk about attaching files to an email. Hmm, but to make it more interesting, let's first learn how to generate a styled PDF.

Leave a comment!

  • 2020-06-30 Victor Bocharsky

    Hey Samuel,

    Thank you for your feedback and confirming that it did the trick for you! I'm really happy :)

    Cheers!

  • 2020-06-29 Samuel Vicent

    Hi Victor,

    First of all thank you for your help, very helpful indeed.

    The scenario is a supervisor but in the essence is like a cron, you are right :)

    Setting in the configuration file framework => default_locale parameter does not work in this case.

    I played around with your idea about WorkerMessageReceivedEvent. It has a lot of sense, because any message to be handled will go through it.

    Setting the _locale to the RequestContext instance works fine :) !!!

    This helps not passing the _locale parameter to every twig url function call, which allows working with twig templates as a default web request. This small big detail helps working the team smoothly and without unexpected errors :)

    In a short period of time we will be translating the website I am working on, so it will be a good moment for refactoring and for considering the same approach for the translator instance (in this case translator->setLocale), so translating a text like 'text-to-be-translated' | trans, will be shorter than writing every time 'text-to-be-translated' | trans([], 'DOMAIN', 'locale')

    Again, thank you very much indeed for your help and I hope this helps to the community.

    Regards! :)

  • 2020-06-12 Victor Bocharsky

    Hey Samuel,

    Ah, yes, I see, it sounds like an edge case. Thanks for mention it! I think the problem is that there's no locale in the request that is handled I suppose by CRON in your case? So yes, request context helps with host and scheme as you set it explicitly via parameters, but it does not help with locale. Hm, but do you have a default locale set? If the locale wasn't specified explicitly, it should use default locale. I'd suppose it should work out of the box, but probably it's not.

    Unfortunately I don't see a specific parameter name that would help with injecting locale, but I see that Symfony\Component\Routing\RequestContext service has setQueryString() method that should work for this, and I think you can create a listener that will inject a locale to that RequestContext. I suppose you can do it for ConsoleEvents::COMMAND event and probably for WorkerMessageReceivedEvent::class one to make it work with Cron.

    At least, you can try to check it - add that listener for command, generate a URL and "dump(); die;" it.

    I hope this helps!

    Cheers!

  • 2020-06-11 Samuel Vicent

    I'd like to share a rare issue when sending async e-mails using the url twig function.

    I always get the error: Unable to generate a URL for the named route "THE_ROUTE_NAME" as such route does not exist.

    The project I'm working on has all routes prefixed with /en-en, like this:


    controllers:
    resource: ../../src/App/Controller/
    type: annotation
    prefix:
    es-es: '/es-es'

    And I also have in the parameters file (where SITE_BASE_SCHEME and SITE_BASE_HOST are defined in a .env file):


    router.request_context.scheme: '%env(SITE_BASE_SCHEME)%'
    router.request_context.host: '%env(SITE_BASE_HOST)%'

    For this error to not be thrown I have to explicitly pass to the twig url function the _locale parameter, for example: url('frontend_legal_terms', {'_locale': locale})

    I wonder if there is a better way to use the url twig function without passing the _locale parameter explicitly.

    Many thanks in advance!