Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Uploading References

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

Unlike the main form on this page, this form will submit to a different endpoint. And instead of continuing to put more things into ArticleAdminController, let's create a new controller for everything related to article references: ArticleReferenceAdminController. Extend BaseController - that's just a small base controller we created in our Symfony series: it extends the normal AbstractController. So nothing magic happening there.

<?php
namespace App\Controller;
... lines 4 - 9
class ArticleReferenceAdminController extends BaseController
{
... lines 12 - 19
}

The Upload Endpoint

Back in the new class, create public function uploadArticleReference() and, above, @Route: make sure to get the one from Symfony/Component. Set the URL to, how about, /admin/article/{id}/references - where the {id} is the Article id that we want to attach the reference to. Add name="admin_article_add_reference". Oh, and let's also set methods={"POST"}.

... lines 1 - 4
use App\Entity\Article;
... line 6
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class ArticleReferenceAdminController extends BaseController
{
/**
* @Route("/admin/article/{id}/references", name="admin_article_add_reference", methods={"POST"})
... line 14
*/
public function uploadArticleReference(Article $article, Request $request)
{
... line 18
}
}

That's optional, but it'll let us create another endpoint later with the same URL that can be used to fetch all the references for a single article.

Let's keep going! Because the article {id} is in the URL, add an Article $article argument. Oh, and we need security! You can only upload a file if you have access to edit this article. In our app, we check that with this @IsGranted("MANAGE", subject="article") annotation, which leverages a custom voter that we created in our Symfony series. It basically makes sure that you are the author of this article or a super admin.

... lines 1 - 5
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 7 - 9
class ArticleReferenceAdminController extends BaseController
{
/**
* @Route("/admin/article/{id}/references", name="admin_article_add_reference", methods={"POST"})
* @IsGranted("MANAGE", subject="article")
*/
public function uploadArticleReference(Article $article, Request $request)
{
... line 18
}
}

Finally, we're ready to fetch the file: add the Request argument - the one from HttpFoundation - and let's dd($request->files->get()) and then the name from the input field: reference.

... lines 1 - 9
class ArticleReferenceAdminController extends BaseController
{
/**
* @Route("/admin/article/{id}/references", name="admin_article_add_reference", methods={"POST"})
* @IsGranted("MANAGE", subject="article")
*/
public function uploadArticleReference(Article $article, Request $request)
{
dd($request->files->get('reference'));
}
}

Solid start. Copy the route name and head back to the template. Set the action attribute to {{ path() }}, the route name, and for the placeholder part, I'll use multiple lines and pass id set to article.id. Oh wait... we don't have an article variable inside this template. We do have the articleForm variable, and we could get the Article from that... but to shorten things, let's properly pass it in.

... lines 1 - 2
{% block content_body %}
... lines 4 - 7
<div class="row">
... lines 9 - 14
<div class="col-sm-4">
... lines 16 - 17
<form action="{{ path('admin_article_add_reference', {
id: article.id
}) }}" method="POST" enctype="multipart/form-data">
... lines 21 - 22
</form>
</div>
</div>
{% endblock %}
... lines 27 - 33

Find the edit() action of ArticleAdminController and pass an article variable. Now we can say article.id.

... lines 1 - 17
class ArticleAdminController extends BaseController
{
... lines 20 - 57
public function edit(Article $article, Request $request, EntityManagerInterface $em, UploaderHelper $uploaderHelper)
{
... lines 60 - 82
return $this->render('article_admin/edit.html.twig', [
... line 84
'article' => $article,
]);
}
... lines 88 - 125
}

Phew! Ok, let's check this out: refresh and inspect element on the form. Yep, the URL looks right and the enctype attribute is there. Ok, try it: select the Symfony Best Practices doc and... upload! Yes! It's our favorite UploadedFile object!

These article references are special because we need to keep them private: they should only be accessible to the author or a super admin. The process for uploading & downloading private files is, a bit different.

Setting up UploaderHelper

But, we'll start in very similar way: by opening our favorite service, and all-around nice class, UploaderHelper. Down here, add a new public function uploadArticleReference() that will have a File argument and return a string... pretty much the same as the other method, except that we won't need an $existingFilename because we won't let ArticleReference objects be updated. If you want to upload a modified file - cool! Delete the old ArticleReference and upload a new one. You'll see what I mean as we keep building this out.

... lines 1 - 12
class UploaderHelper
{
... lines 15 - 70
public function uploadArticleReference(File $file): string
{
... line 73
}
... lines 75 - 81
}

To get started, just dd($file).

... lines 1 - 12
class UploaderHelper
{
... lines 15 - 70
public function uploadArticleReference(File $file): string
{
dd($file);
}
... lines 75 - 81
}

Back in the controller, let's finish this whole darn thing. Set the file to an $uploadedFile object and I'll add the same inline documentation that says that this is an UploadedFile object - the one from HttpFoundation.

... lines 1 - 6
use App\Service\UploaderHelper;
use Doctrine\ORM\EntityManagerInterface;
... line 9
use Symfony\Component\HttpFoundation\File\UploadedFile;
... lines 11 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->files->get('reference');
... lines 24 - 37
}
}

Then say $filename =... oh - we don't have the UploaderHelper service yet! Add that argument: UploaderHelper $uploaderHelper. Then $filename = $uploaderHelper->uploadArticleReference($uploadedFile).

... lines 1 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 24
$filename = $uploaderHelper->uploadArticleReference($uploadedFile);
... lines 26 - 37
}
}

We know that won't work yet... but if we use our imagination, we know that... someday, it should return the new filename that was stored on the filesystem. To put this value into the database, we need to create a new ArticleReference object and persist it.

Tightening Up ArticleReference

Oh, but before we do - go open that class. This is a very traditional entity: it has some properties and everything has a getter and a setter. That's great, but because every ArticleReference needs to have its Article property set... and because an ArticleReference will never change articles, find the setArticle() method and... obliterate it!

Instead, add a public function __construct() with a required Article argument. Set that onto the article property. This is an optional step - but it's always nice to think critically about your entities: what methods do you not need?

... lines 1 - 9
class ArticleReference
{
... lines 12 - 39
public function __construct(Article $article)
{
$this->article = $article;
}
... lines 44 - 89
}

Saving ArticleReference & the Original Filename

Back up in our controller, say $articleReference = new ArticleReference() and pass $article. Call $article->setFilename($filename) to store the unique filename where this file was stored on the filesystem.

... lines 1 - 5
use App\Entity\ArticleReference;
... lines 7 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 26
$articleReference = new ArticleReference($article);
$articleReference->setFilename($filename);
... lines 29 - 37
}
}

But remember! There are a couple of new pieces of info that we can set on ArticleReference - like the original filename. Set that to $uploadedFile->getClientOriginalName(). Now, technically this method can return null, though, I'm not actually sure if that's something that can happen in any realistic scenario. But, just in case, add ?? $filename. So, if the client original name is missing for some reason, fall back to $filename.

... lines 1 - 5
use App\Entity\ArticleReference;
... lines 7 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 26
$articleReference = new ArticleReference($article);
$articleReference->setFilename($filename);
$articleReference->setOriginalFilename($uploadedFile->getClientOriginalName() ?? $filename);
... lines 30 - 37
}
}

Finally, just in case we ever want to know what type of file this is, we'll store the file's mime type. Set this to $uploadedFile->getMimeType(). This can also return null - so default it to application/octet-stream, which is sort of a common way to say "I have no idea what this file is".

... lines 1 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 26
$articleReference = new ArticleReference($article);
$articleReference->setFilename($filename);
$articleReference->setOriginalFilename($uploadedFile->getClientOriginalName() ?? $filename);
$articleReference->setMimeType($uploadedFile->getMimeType() ?? 'application/octet-stream');
... lines 31 - 37
}
}

With that done, save this: add the EntityManagerInterface $entityManager argument, then $entityManager->persist($articleReference) and $entityManager->flush().

... lines 1 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 31
$entityManager->persist($articleReference);
$entityManager->flush();
... lines 34 - 37
}
}

Finish with return redirectToRoute() and send the user back to the edit page: admin_article_edit passing this id set to $article->getId().

... lines 1 - 13
class ArticleReferenceAdminController extends BaseController
{
... lines 16 - 19
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager)
{
... lines 22 - 34
return $this->redirectToRoute('admin_article_edit', [
'id' => $article->getId(),
]);
}
}

Yep - that's the route on the edit endpoint.

Alright! With any luck, it should hit our dd() statement. Go back to your browser: I already have the Symfony Best Practices PDF selected. Hit update... yea! UploadedFile coming from UploaderHelper.

Next: let's move the uploaded file... except that... we can't move it using the filesystem service object we have now... because we can't store these private files in the public/ directory. Hmm...

Leave a comment!

3
Login or Register to join the conversation
Default user avatar
Default user avatar elbarto | posted 3 years ago | edited

Hey, nice serie :)

I have an issue and idk what to do, i followed the tutorial until this chapter and now i dont need to make my files private at all.
So in my ArticleReferenceController once i called my uploadHelper, the file is successfully moved with the $file->move($destination, $newFilename) method and returns the filename. But when i want to proceed and save the information to the database i get this error :

<b>The "C:\wamp64\tmp\php8134.tmp" file does not exist or is not readable.</b>

Which is raised by the $uploadedFile->getMimeType() method. I did raise the php upload limit in my php.ini file but it doesnt change anything. Im trying to upload a different img each time, but nothing works, always that same error poping !!

Would really appreciate your help !

Reply
Default user avatar
Default user avatar elbarto | elbarto | posted 3 years ago | edited

Also heres the result of my php -i | grep upload :

file_uploads => On => On<br />max_file_uploads => 30 => 30<br />upload_max_filesize => 300M => 300M<br />upload_tmp_dir => c:/wamp64/tmp => c:/wamp64/tmp

Reply

Hey @elbarto

If the problem is related to the file size, then you forgot to update this setting as well post_max_size, just increase it higher than your file size. If it's still failing then it's not a file size issue. Let me know if it works. 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