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 SubscribeWhen we go to the show page... of course, it doesn't work yet! We need to update the template. Copy the uploaded_asset()
code, open show.html.twig
... here it is, and paste.
... lines 1 - 4 | |
{% block content_body %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<img class="show-article-img" src="{{ uploaded_asset(article.imagePath) }}"> | |
... lines 9 - 25 | |
</div> | |
</div> | |
... lines 28 - 78 | |
{% endblock %} | |
... lines 80 - 86 |
Easy! Reload the page now. Oh... it still doesn't work. Inspect element on the image. Ah, the path is right, but because there is no /
at the beginning, and because the current URL is a sort of sub-directory, it's looking for the image in the wrong place. If you hack in the /
... it pops up!
Adding this opening slash is actually one of the jobs of the asset()
function. Try this: wrap this entire thing in asset()
.
... lines 1 - 4 | |
{% block content_body %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<img class="show-article-img" src="{{ asset(uploaded_asset(article.imagePath)) }}"> | |
... lines 9 - 25 | |
</div> | |
</div> | |
... lines 28 - 78 | |
{% endblock %} | |
... lines 80 - 86 |
Now refresh. It works! But, wrapping asset()
around uploaded_asset()
is kind of annoying: can't we just handle this internally in UploaderHelper
?
... lines 1 - 4 | |
{% block content_body %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<img class="show-article-img" src="{{ uploaded_asset(article.imagePath) }}"> | |
... lines 9 - 25 | |
</div> | |
</div> | |
... lines 28 - 78 | |
{% endblock %} | |
... lines 80 - 86 |
After all, this method is supposed to return the public path to an asset: we shouldn't need to do any other "fixes" on the path after.
The easiest way to fix things would be to add a /
at the beginning. That would totally work! But... allow me to nerd-out for a minute and explain an edge-case that the asset()
function usually handles for us. Imagine if your site were deployed under a subdirectory of a domain. Like, instead of the URL on production being thespacebar.com
, it's thegalaxy.org/thespacebar
- our app does not live at the root of the domain. If you have a situation like this, hardcoding a /
at the beginning of the URL won't work! It would need to be /thespacebar/
.
The asset()
function does this automatically: it detects that subdirectory and... just handles it! To really make our getPublicPath()
shine, I want to do the same thing here.
To do this, we're going to work with a service that you don't see very often in Symfony: it's the service that's used internally by the asset()
function to determine the subdirectory. In the constructor, add another argument: RequestStackContext $requestStackContext
. I'll hit Alt + Enter
and select initialize fields to create that property and set it.
... lines 1 - 5 | |
use Symfony\Component\Asset\Context\RequestStackContext; | |
... lines 7 - 8 | |
class UploaderHelper | |
{ | |
... lines 11 - 16 | |
public function __construct(string $uploadsPath, RequestStackContext $requestStackContext) | |
{ | |
... line 19 | |
$this->requestStackContext = $requestStackContext; | |
} | |
... lines 22 - 43 | |
} |
Down in getPublicPath()
, return $this->requestStackContext->getBasePath()
and then '/uploads/'.$path
.
... lines 1 - 8 | |
class UploaderHelper | |
{ | |
... lines 11 - 37 | |
public function getPublicPath(string $path): string | |
{ | |
// needed if you deploy under a subdirectory | |
return $this->requestStackContext | |
->getBasePath().'/uploads/'.$path; | |
} | |
} |
If our app lives at the root of the domain - like it does right now - this will just return and empty string. But if it lives at a subdirectory like thespacebar
, it'll return /thespacebar
.
Try it! Oh... wow - huge error! This RequestStackContext
service is such a low-level service, that Symfony doesn't make it available to be used for autowiring. Check out the error, it says:
Yo! You can't autowire the
$requestStackContext
argument: it's type-hinted with a class calledRequestStackContext
, but there isn't a service with this id. Maybe you can create a service alias for this class that points to theassets.context
service.
This is a bit technical and we talk about this in our Symfony Fundamentals course. Symfony sees that the RequestStackContext
type-hint is not autowireable, but it also sees that there is a service in the container - called assets.context
- that is an instance of this class!
Check it out: copy the full class name and then go into config/services.yaml
. At the bottom, paste the full class name, go copy the service id they suggested, and say @assets.context
.
... lines 1 - 9 | |
services: | |
... lines 11 - 47 | |
Symfony\Component\Asset\Context\RequestStackContext: '@assets.context' |
This creates a service alias. Basically, there is now a new service that lives in the container called Symfony\Component\Asset\Context\RequestStackContext
. And if you fetch it, it'll really just give you the assets.context
service. The key thing is that this makes the class autowireable.
To prove it, find your terminal and run:
php bin/console debug:autowiring request
to search for all autowireable classes that contain that string. Hey! There is our RequestStackContext
! If we had run this a minute ago, it would not have been there.
Refresh the page now. Got it! And if you look at the path, yep! It's /uploads/article_image/astronaut.jpeg
. If we lived under a subdirectory, that subdirectory would be there. Small detail, but our site is still super portable.
Next, let's create thumbnails of our image so the user doesn't need to download the full size.
Hey YinYang,
And that's OK, right? What do you think it should return? See the explanation below:
> If our app lives at the root of the domain - like it does right now - this will just return and empty string. But if it lives at a subdirectory like thespacebar, it'll return /thespacebar.
So, if your project is not in a subdirectory - that's OK :)
Cheers!
Hello! I couldn't find information online why it's better to use RequestStackContext over RequestStack, that is available for autowiring.
One could then do:$this->requestStack->getCurrentRequest()->getBasePath()
(which seems to be preferred: https://symfony.com/doc/current/service_container/request.html)
Instead of:$this->requestStackContext->getBasePath()
Also there's Request we could have used. It's unclear for me what pointed you in that direction: the documentation doesn't even mention it. Please help :)
Yo Renaud G.!
Really interesting question :). In practice, both are probably fine. I used RequestStackContext because technically that's what the Twig asset()
function uses internally. And so, we're exactly matching its behavior.
Here's what happens behind the scenes - it should help clarify a bit:
1) Symfony boots up and creates a Request object with all the data inside
2) When Symfony's router executes, it uses the data from the Request to set the data on the RequestContext - https://github.com/symfony/symfony/blob/e60a876201b5b306d0c81a24d9a3db997192079c/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php#L77-L86 - that means that (in practice) the data in Request and RequestStack are identical.
That's why... probably both will work fine. However, there is at least one edge case: running code inside a custom console command. If you run code inside a custom console command and need to generate absolute URLs, because there is no request, generating absolute URLs doesn't work (the domain "localhost" will be used). To work around this, you need to set two parameters - we show that here: https://symfonycasts.com/screencast/mailer/route-context#codeblock-414154c720
The point is, when you do that, that data is set on the request context not the request. In other words, if you correctly added these parameters so that your custom console command behaves correctly but then rely on the Request inside of the code in this video (instead of the RequestContext) it would not work.
The tl;dr is: we use request context because that's what asset()
uses internally... and it guarantees our code behaves exactly like it :).
Cheers!
Thanks Ryan! I'm still having a hard time wrapping my head around how you went from asset to RequestStackContext. Asset pointed me to Packages that pointed me to Package that has a ContextInterface that is apparently instantiated at runtime with a RequestStackContext but oh boy how should I have known that?
Some concepts are still a bit opaque to me and make me wonder what else I might be doing wrong unknowingly.
Hey Renaud G.!
I'm still having a hard time wrapping my head around how you went from asset to RequestStackContext
This is a particularly tricky/deep one to figure out.
Asset pointed me to Packages that pointed me to Package that has a ContextInterface that is apparently instantiated at runtime with a RequestStackContext but oh boy how should I have known that?
Well done following the trail! I'm impressed! Once I aw that some Package object/service had a ContextInterface argument, I would be wondering: "I wonder what service that "ContextInterface" argument truly is"? I personally then dig into core XML files in Symfony to find the service definition (you can also grep symfony/vendor for this interface to help find those), but you can usually also get help from bin/console debug:container
. Something like this series of commands:
./bin/console debug:container | grep Package
./bin/console debug:container assets.packages --show-arguments
./bin/console debug:container assets._default_package --show-arguments
./bin/console debug:container assets.context --show-arguments
Try those commands and see if you can follow how I'm jumping from level to level. The last command ultimately tells me that the ContextInterface argument is really the service id assets.context
, which is an instance of Symfony\Component\Asset\Context\RequestStackContext
. THAT is how I dig up what low-level service does this work. It's one of the most difficult examples of "trying to figure out what Symfony is really doing behind the scenes". So honestly, I think you're doing really well :).
Cheers!
// 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
}
}
Hi,
When I do a dump on
$this->requestStackContext->getBasePath()
,it returns me a empty string... :/