Flysystem <3 LiipImagineBundle
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeFlysystem is killing it for us! But... there's a problem hiding... like, a it-won't-actually-work-in-the-real-world kind of problem. Yikes! In theory, we should be able to go into the oneup_flysystem.yaml
file right now, change the adapter to S3 and everything would work. In theory.
How LiipImagineBundle Finds Images
The problem is LiipImagineBundle. Open up templates/article/homepage.html.twig
: we call uploaded_asset()
, pass that article.imagePath
and that value is passed into imagine_filter
. So basically, a string like uploads/article_image/something.jpg
is passed to the filter.
The problem? By default, LiipImagineBundle reads the source image file from the filesystem. If we refactored to use S3... well... imagine would be looking in the wrong place!
You can see this by running:
php bin/console debug:config liip_imagine
This is the current config for this bundle, which includes all of its default values. Near the bottom, see that "loaders" section? The "loader" is the piece that's responsible for reading the source image. It defaults to using the filesystem and it knows to look in the public/
directory! So when we pass it upload/article_image/
some filename, it finds it perfectly. Well... it works until our files don't live on the server anymore.
The solution? We need this to use Flysystem.
Flysystem Loader
Let's go to back to the LiipImagineBundle documentation: find their GitHub page and then click down here on the "Download the Bundle" link as an easy way to get into their full docs. Now, go back to the main page and... down here near the bottom, it talks about different "data loaders". The default is "File System", we want Flysystem.
Let's see... yea, we've already installed the bundle. Copy this loaders section - we already have our Flysystem config all set up. Then, open our liip_imagine.yaml
file and, really, anywhere, paste!
This creates a loader called profile_photos
- that name can be anything. Let's use flysystem_loader
. The critical part is the key flysystem
: that says to use the "Flysystem" loader that comes with the bundle. The only thing it needs to know, is the service id of the filesystem that we want to use.
liip_imagine: | |
// ... lines 2 - 5 | |
loaders: | |
flysystem_loader: | |
flysystem: | |
// ... lines 9 - 57 |
For that, go back to config/services.yaml
and copy the long service id from the bind
section. Back in liip_imagine.yaml
, paste!
liip_imagine: | |
// ... lines 2 - 5 | |
loaders: | |
flysystem_loader: | |
flysystem: | |
filesystem_service: oneup_flysystem.public_uploads_filesystem_filesystem | |
// ... lines 10 - 57 |
We now have a "loader" called flysystem_loader
, and a "loader's" job is to... ya know, "load" the source file. You can technically have multiple loaders, though I've never had to do that. To always have LiipImagineBundle load the files via Flysystem, below, add data_loader
set to the loader's name: flysystem_loader
. I'll add a comment:
default loader to use for all filter sets
liip_imagine: | |
// ... lines 2 - 5 | |
loaders: | |
flysystem_loader: | |
flysystem: | |
filesystem_service: oneup_flysystem.public_uploads_filesystem_filesystem | |
# default loader to use for all filter sets | |
data_loader: flysystem_loader | |
// ... lines 13 - 57 |
Because, you can technically specify which loader you want to use on each filterset. Again, I've never had to do that: we always want to use flysystem.
Cool! Let's try it! Go into the public/
directory... let me find it... and delete all the existing thumbnails - let's delete media/cache/
entirely. By doing this, the bundle will use the data loader to get the contents of each image so that it can recreate the thumbnails.
Correcting the Path to LiipImagineBundle
Testing time! Let's go back to, how about, the homepage. And... it doesn't work. Drat! Inspect element. Hmm, it does start with the media/cache/resolve
part. Then, the path at the end is - uploads/article_image/lightspeed...png
. That's the path that we're passing to the filter.
Go back to the homepage template. The problem now - and it's really cool - is that we told LiipImagineBundle to use Flysystem to load files... but the root of our filesystem is the public/uploads
directory. In other words, if you want to read a file from our filesystem, the path needs to be relative to this directory. In other words, it should not contain the uploads/
part
The fix? Remove the uploaded_asset()
function: we can just pass article.imagePath
, which will be article_image/
the filename.
// ... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
// ... lines 6 - 8 | |
<div class="col-sm-12 col-md-8"> | |
// ... lines 10 - 21 | |
<div class="article-container my-1"> | |
<a href="{{ path('article_show', {slug: article.slug}) }}"> | |
<img class="article-img" src="{{ article.imagePath|imagine_filter('squared_thumbnail_small') }}"> | |
// ... lines 25 - 37 | |
</a> | |
</div> | |
// ... line 40 | |
</div> | |
// ... lines 42 - 61 | |
</div> | |
</div> | |
{% endblock %} |
I love this! Need to thumbnail something? Just pass it the Flysystem path: you don't need the word uploads
or anything like that. The uploaded_asset()
function will still be useful if you want the public path to an asset without thumbnailing, but if you're using imagine_filter
, passing the short, relative path is all you need.
Try it! Refresh! It still doesn't work? Oh yea! A few minutes ago, we deleted all of the original images from the fixtures. But we did re-upload a few of them. So if you scroll down... here we go - here's the Earth image we uploaded. So, it is now working perfectly.
Let's reload our fixtures to make sure:
php bin/console doctrine:fixtures:load
Now the homepage... yes - everything is here. Let's make the same change in the other two places we're thumbnailing. Click onto the show page. This lives in templates/article/show.html.twig
: remove uploaded_asset
there. Refresh... good!
// ... lines 1 - 4 | |
{% block content_body %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<img class="show-article-img" src="{{ article.imagePath|imagine_filter('squared_thumbnail_medium') }}"> | |
// ... lines 9 - 25 | |
</div> | |
</div> | |
// ... lines 28 - 78 | |
{% endblock %} | |
// ... lines 80 - 86 |
For the other one, go back to the admin article section - log back in with password "engage", because we reloaded the database. When we're editing an image, yep, also broken.
Find this in templates/article_admin/_form.html.twig
: take off uploaded_asset()
.
{{ form_start(articleForm) }} | |
// ... lines 2 - 5 | |
<div class="row"> | |
// ... lines 7 - 13 | |
<div class="col-sm-3"> | |
{% if articleForm.vars.data.imageFilename %} | |
<img src="{{ articleForm.vars.data.imagePath|imagine_filter('squared_thumbnail_small') }}" height="100"> | |
{% endif %} | |
</div> | |
</div> | |
// ... lines 20 - 38 | |
{{ form_end(articleForm) }} |
And... got it!
The Resolver: Saving the Images to Flysystem
So, the "data loader" is responsible for reading the original image. But, there's another important concept from LiipImagineBundle called "resolvers". Click down on the "Flysystem Resolver" in their docs. The resolver is responsible for saving the thumbnail image back to the filesystem after all of the transformations. By default, no surprise, LiipImagineBundle writes things directly to the filesystem. So even if we moved Flysystem to s3, LiipImagineBundle would still be writing the thumbnail files back to our server - into the public/media
directory.
Tip
You can also completely offload the processing and storage of your files to a cloud service like rokka.io by leveraging LiipRokkaImagineBundle.
Let's change that! In the docs, copy the resolvers
section. Back in our liip_imagine.yaml
file, paste that. It's pretty much the same as before: we'll call it flysystem_resolver
and tell it to save the images using the same filesystem service. Remove visibility
- that sets the Flysystem visibility, which is a concept we'll talk about soon. True is the default value anyways, which basically means these files will be publicly accessible.
liip_imagine: | |
// ... lines 2 - 13 | |
resolvers: | |
flysystem_resolver: | |
flysystem: | |
filesystem_service: oneup_flysystem.public_uploads_filesystem_filesystem | |
cache_prefix: media/cache | |
root_url: /uploads | |
// ... lines 21 - 67 |
cache_prefix
is the subdirectory within the filesystem where the files should be stored and root_url
is the URL that all the paths will be prefixed with when the image paths are rendered. Right now, it needs to be /uploads
.
For example, if LiipImagineBundle stores a file called media/cache/foo.jpg
into Flysystem, we know that the public path to this will be /uploads/media/cache/foo.jpg
. We'll talk more about this setting later when we move to s3.
Ok, delete the media/
directory entirely. Oh, and I almost forgot the last step: add cache
set to flysystem_resolver
- let's put an "r" on that.
liip_imagine: | |
// ... lines 2 - 13 | |
resolvers: | |
flysystem_resolver: | |
flysystem: | |
filesystem_service: oneup_flysystem.public_uploads_filesystem_filesystem | |
cache_prefix: media/cache | |
root_url: /uploads | |
# default cache resolver for saving thumbnails | |
cache: flysystem_resolver | |
// ... lines 23 - 67 |
This tells the bundle to always use this resolver. I'm not sure why it's called "cache" - the bundle seems to use "resolver" and "cache" to describe this one concept.
Ok! Moment of truth! Refresh. Ha! It works! Go check out where the thumbnails are stored: there is no media/
directory anymore! The Flysystem filesystem points to the public/uploads
directory, so the media/cache
directory lives there. And thanks to the /uploads
root_url
, when it renders the path, it knows to start with /uploads
and then the path in Flysystem.
I love this! It's a bit tricky to get these two libraries to play together perfectly. But now we are much more prepared to switch between local uploads and S3.
Next: we can generate public URLs to thumbnailed files or the original files. But, what if you need to force all the URLs to include the domain name? This is something you don't think about until you need to generate a PDF or send an email from a console command or worker. Then... it can be a nightmare. Let's add this to our asset system in a way that we love.
If you are using the
league/flysystem-bundle
Flysystem bundle, which is created by the authors of Flysystem and seems to be more actively maintained, your configuration will look a little different:The way this bundle's configuration works is a little more straightforward in my opinion, but the LiipImagine bundle does detail configuration options for both bundles in their docs.