Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Absolute Asset Paths

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 $10.00

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

Login Subscribe

One of the things I've noticed is that this word uploads - the directory where uploads are being stored - is starting to show up in a few places. We have it here in our liip_imagine config file, the oneup_flysystem.yaml file and in UploaderHelper: it's used in getPublicPath().

Centralizing the uploads/ Path

It's not a huge problem, but repetition is a bummer and this will cause some issues when moving to S3: we'll need to hunt down all of these paths and change them to point to the S3 domain.

Let's tighten this up. In services.yaml, create two new parameters: The first will be uploads_dir_name set to uploads - this is the name of the directory where we are storing uploaded files. Call the second one uploads_base_url and set this to almost the same thing: / and then %uploads_dir_name%. This represents the base URL to the uploaded assets.

... lines 1 - 5
parameters:
... lines 7 - 8
uploads_dir_name: 'uploads'
uploads_base_url: '/%uploads_dir_name%'
... lines 11 - 51

Thanks to these, we can do some cleanup! In liip_imagine.yaml, we need the URL. Copy uploads_base_url and then use %uploads_base_url%.

liip_imagine:
... lines 2 - 13
resolvers:
flysystem_resolver:
flysystem:
... lines 17 - 18
root_url: '%uploads_base_url%'
... lines 20 - 67

Next, in oneup_flysystem.yaml, we need the directory name. Copy the other parameter: %uploads_dir_name%.

... line 1
oneup_flysystem:
adapters:
public_uploads_adapter:
local:
directory: '%kernel.project_dir%/public/%uploads_dir_name%'
... lines 7 - 10

The last place is in UploaderHelper. The getBasePath() call will give us the directory where the site is installed - usually an empty string. Then we need to pass in the uploads_base_url parameter.

Add a new argument to the constructor: string $uploadedAssetsBaseUrl. I'll create the property by hand and give it a slightly different name: $publicAssetBaseUrl, not for any particular reason. Set that in the constructor:

... lines 1 - 12
class UploaderHelper
{
... lines 15 - 22
private $publicAssetBaseUrl;
public function __construct(FilesystemInterface $publicUploadsFilesystem, RequestStackContext $requestStackContext, LoggerInterface $logger, string $uploadedAssetsBaseUrl)
{
... lines 27 - 29
$this->publicAssetBaseUrl = $uploadedAssetsBaseUrl;
}
... lines 32 - 78

Back in getPublicPath(), use this: getBasePath() then $this->publicAssetsBaseUrl, which will contain the / at the beginning.

... lines 1 - 12
class UploaderHelper
{
... lines 15 - 70
public function getPublicPath(string $path): string
{
// needed if you deploy under a subdirectory
return $this->requestStackContext
->getBasePath().$this->publicAssetBaseUrl.'/'.$path;
}
}

Cool! But, Symfony will not be able to autowire this string argument. You can see the error if you try to reload any page. Yep!

We know how to fix that: back in services.yaml, add a bind: $uploadedAssetsBaseUrl set to %uploads_base_url%. Now... it works!

... lines 1 - 11
services:
# default configuration for services in *this* file
_defaults:
... lines 15 - 21
bind:
... lines 23 - 25
$uploadedAssetsBaseUrl: '%uploads_base_url%'
... lines 27 - 51

Linking to the Full Image

Small step, but with all this config in one spot, we can do something kinda cool... with almost no effort. But first, I want to triple check that all this public path stuff is setup correctly. Our getPublicPath() method is currently used in one spot: by the uploaded_asset() Twig function. But, we're not actually using this Twig function anywhere at the moment.

So try this: in the form, we're showing the thumbnail. It might be useful to allow the user to click this and see the original image. That's pretty easy: add <a href=""> and use uploaded_asset(articleForm.vars.data.imagePath).

That's it! Wrap this around the img tag and let's also add target="_blank".

{{ form_start(articleForm) }}
... lines 2 - 5
<div class="row">
... lines 7 - 13
<div class="col-sm-3">
{% if articleForm.vars.data.imageFilename %}
<a href="{{ uploaded_asset(articleForm.vars.data.imagePath) }}" target="_blank">
<img src="{{ articleForm.vars.data.imagePath|imagine_filter('squared_thumbnail_small') }}" height="100">
</a>
{% endif %}
</div>
</div>
... lines 22 - 40
{{ form_end(articleForm) }}

Cool. Test that - refresh and... click. Nice! This sends us directly to the source image.

Absolute URLs

Thanks to our setup, we can now solve a really annoying problem. Inspect element on the image: notice that both the href and the image src paths do not contain the domain name. That's not a problem at all in a normal web context. But if you ever try to render a page into a PDF with something like wkhtmltopdf or create a console command to send an email that references an uploaded file, well... suddenly, those paths will start to break! In those contexts, you need the URLs to be absolute.

There are a few ways to solve this... and honestly, I went back and forth on the best approach. I finally settled on something that we've used here on SymfonyCasts for years. Open your .env file. We're going to create a brand new, custom environment variable called SITE_BASE_URL. Set the default value to https://localhost:8000.

35 lines .env
... lines 1 - 33
SITE_BASE_URL=https://localhost:8000

Remember: this file is committed to git, so this is the default value. You can create a .env.local file to override this value locally or on production. Or, of course, if it's easy, you can override this by setting a real environment variable.

Next, go back to services.yaml. And for the uploads_base_url, use %env()% and inside, SITE_BASE_URL: that's the syntax for referencing an environment variable.

... lines 1 - 5
parameters:
... lines 7 - 9
uploads_base_url: '%env(SITE_BASE_URL)%/%uploads_dir_name%'
... lines 11 - 51

And... just like that - every single path to every single uploaded asset will now be absolute. Seriously! Test it out! Boom! Both the link href and the image src contain the https://localhost:8000 part.

And, sure, you could add some config so that you could turn this on only when you need it... but I don't really see the point. I'll keep absolute URLs always.

Next: let's start uploading private assets: stuff that can't be put into the public/ directory because we need to check security before we let a user download it.

Leave a comment!

12
Login or Register to join the conversation
Default user avatar
Default user avatar Matteo Maranta | posted 2 years ago
The getBasePath() call will give us the directory where the site is installed - usually an empty string.

This doesn't turn out true in production to me, where getBasePath gives me exactly the same string i put in the SITE_BASE_URL; am I missing something?

Reply

Hey Matteo Maranta

I think that's the right behavior (unless I misunderstood something), you're seeing your env var SITE_BASE_URL because it's being used in the getBasePath() method

Cheers!

Reply
Volodymyr T. Avatar
Volodymyr T. Avatar Volodymyr T. | posted 3 years ago

Please provide a link to the preferred wkhtmltopdf package/bundle for symfony, as long as you mention it (which is a 100% great way to learn about new tools).

And, what exactly is the other approaches Ryan mentions at 4:34? I'd like to be able to make an informed decision/choice on this matter.

Reply

Hey Voltel,

Our advice is KnpSnappyBundle: https://github.com/KnpLabs/... - that just a wrapper around Wkhtmltopdf tool. And looks like it' the most popular bundle for Symfony that works with PDF.

Unfortunately, I'm not sure what Ryan had on mind at that second, but I'd say that env vars approach is probably the best option. Another option would be to hardcode URLs, but that's not a good option.

Cheers!

1 Reply

Hi victor!

About the other approach around 4:34, I don't remember exactly (to be honest), but I believe that it mostly involved different approaches to using the SITE_BASE_URL. In Symfony, there is an idea of "asset packages" and you can define a base URL for each "asset package". Then, when you use the asset() function (or use that same code in PHP), that package will automatically add the base URL. I think this was the other approach I played with. In the end, I didn't choose that because it's... just a bit more confusing to explain. The approach I used is pretty direct: we create a SITE_BASE_URL variable and then use it in our code. We're not leveraging any, sort of, "invisible magic" to get the job done.

Cheers!

1 Reply
Raed Avatar

Hi team !

I'm little confused between when to use % and @ to autowire services in services.yaml ?
Could you please put some light on it ! Thanks a lot !

Reply

Hey Raed

The @ character is used to indicate Symfony that you want to inject a service

The % sign indicates Symfony that you want to inject a parameter or environment variable through using the %env(SOME_VARIABLE)% notation

Does it makes more sense to you now?
Cheers!

Reply
Raed Avatar
Raed Avatar Raed | MolloKhan | posted 3 years ago | edited

Hey MolloKhan,

Thank you so much for this very helpful and clear clarification.

1 Reply

Personally I would like to avoid hardcoding that value and, instead, use `{{ app.request.schemeAndHttpHost }}` to fix that or create my own Twig filter/function. On my own website, radiolista.pl, which is open source, I even use <base> tag: https://github.com/TomaszGa... I don't know whether wkhtmltopdf and email clients support <base>, although.

Reply

Hey Tomasz,

What exactly hardcoded value are you talking about? If you're about SITE_BASE_URL - it's not exactly hardcoded value, it's dynamic variable.

Cheers!

1 Reply

I mean for me it seems to be better to rely on Symfony magic and mechanism already implemented in Request class instead "hardcoding" it into environment variable which you have to always fill in before starting real work. It's personal, although. :)

Reply

Hey Tomasz,

Ah, yes, I see what you mean :) Personally, I agree with you and I'm always trying to achieve it with app.request things :) But! Sometimes we need to have that HTTP host somewhere in the config, like in our case here where we set it uploads_base_url parameter, i.e. sometimes we need to have that parameter hardcoded. Or for host specific routes, etc. Yes, it causes some incoveniences that you have to tweak that parameter on some servers to get project working, but still it's done in one spot only, you don't have to change all possible hardcoded URLs all over the project code as we use that env var values everywhere, so just need to tweak that env var.

I hope this makes sense for you! But I agree, if you can avoid using such env var in favor of using app.request in your project - that would be even better.

Cheers!

Reply
Cat in space

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

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "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-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}
userVoice