File uploads have always been tricky, and handling them gracefully and painless is a challenging task. Avoid duplicates, handle removal and replacing without leaving orphan files somewhere in the database or on the server, showing a nice interface to the user, avoiding the ugly standard grey upload button … a lot of things need to be taken into account when working with file uploads. Luckily, Symfony has some great mechanisms which allow easing the task. In this tutorial I will be describing how I handle multiple file uploads using doctrine events and collection type form fields.
This scenario for this tutorial is a simple one : Folder and Document management. A Folder may contain a number of Documents, whereas a Document can only belong, and thus be uploaded, to a singe Folder. At any time during the Folder creation or modification, we need to be able to add or remove Documents from the Folder.
Aims of this tutorial :
- Create a flexible multiple file upload, which takes advantage of Doctrine Events and CollectionType form fields
- Handle file saving and deleting using only Doctrine Events, thus avoiding adding any extra task in the Controller
Resources :
- OpenClassrooms.com
- CollectionType Field (Symfony official documentation)
Symfony version : 3.4 (LTS), but should work with all 3+ versions
Let’s get started !
In this tutorial we will only be handling Folder objects directly. All the document manipulation is done indirectly, using events and forms. The actions we need to implement are the following :
- adding documents to a folder
- removing documents from a folder
- creating folders and adding documents to them
- deleting folders and their associated documents
Part 1 : Entities
Our two main entities, Folder and Document, can be represented as follows :
A Document has a name and an extension, and it belongs to a single Folder (mandatory). Therefore, we have a @ManyToOne relationship between a Document and its corresponding Folder.
A Folder, on the other hand, only needs a name, and it will hold a collection of Documents. Therefore, we are inverting the previous @ManyToOne relationship by using a @OneToMany relationship between a Folder and all its corresponding Documents.
The Folder Entity relationship :
<?php /** * Folder * * @ORM\Table(name="cookiejar_folder") * @ORM\Entity(repositoryClass="Playground\CookiejarBundle\Repository\FolderRepository") */ class Folder { ... /** * Many Documents have one (the same) Folder * @ORM\OneToMany(targetEntity="Document", mappedBy="folder", cascade={"persist"}, orphanRemoval=true) */ private $documents; ... /** * Add document * * @param \Playground\CookiejarBundle\Entity\Document $document * * @return Folder */ public function addDocument(\Playground\CookiejarBundle\Entity\Document $document) { // Bidirectional Ownership $document->setFolder($this); $this->documents[] = $document; return $this; } /** * Remove document * * @param \Playground\CookiejarBundle\Entity\Document $document */ public function removeDocument(\Playground\CookiejarBundle\Entity\Document $document) { $this->documents->removeElement($document); } /** * Get documents * * @return \Doctrine\Common\Collections\Collection */ public function getDocuments() { return $this->documents; } /** * Constructor */ public function __construct() { $this->documents = new \Doctrine\Common\Collections\ArrayCollection(); } }
Two things are particularly important here :
<?php /** * Many Documents have one (the same) Folder * @ORM\OneToMany(targetEntity="Document", mappedBy="folder", cascade={"persist"}, orphanRemoval=true) */ private $documents;
We only manipulate Folders. So we are only persisting Folder objects. Thus, we need to cascade persist our Documents.
Also, the orphanRemoval=true
option assures that all Document objects which are removed from the array of Documents of a Folder, are also removed from the database. More details on the orphanRemoval option in Doctrine.
<?php /** * Add document * * @param \Playground\CookiejarBundle\Entity\Document $document * * @return Folder */ public function addDocument(\Playground\CookiejarBundle\Entity\Document $document) { // Bidirectional Ownership $document->setFolder($this); $this->documents[] = $document; return $this; }
Again, since we only manipulating Folders, we need to make the Folder entity our owning side. Therefore, we must update the inverse side manually. More details on Owing and Inverse sides in Doctrine.
The Document Entity Class is a little more complicated. It contains a special setup that I use almost every time I need to handle file submissions. This method is described in this (complete) Symfony course.
Basically, we are handling all file manipulations using Doctrine Events. A file is stored in the database using three elements :
- its id, which is the name of the physical file
- its extension
- the file’s original name, as it was on the user’s device
This means that after uploading and persisting helloWorld.pdf, we will have an entry in the database with
(id=5, extension=’.pdf’, name=’helloWorld’)
and a physical file in our upload folder named “5.pdf”.
This solves the existing file name problem without using any fancy algorithms for random name generation.
Document Entity class contains 4 functions which are triggered by Doctrine events. The first two are triggered by the creation or the update of the object, while the last two are triggered by the removal of an object. Before persisting or updating the object, we save the name and the extension of the uploaded file into the object. After persisting or updating the object we remove old files if necessary and we copy the file to the proper location on the server. Removal is handled in a similar way. Before remove, we store a copy of the file name to delete, and after removal we delete the actual file of the disk.
There are numerous advantages to this method. So important that they require a dedicated article 😉 (again, I redirect all those who are interested in all the details and the benefits of this implementation to the original Symfony course).
<?php namespace Playground\CookiejarBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Document * * @ORM\Table(name="cookiejar_document") * @ORM\Entity(repositoryClass="Playground\CookiejarBundle\Repository\DocumentRepository") * @ORM\HasLifecycleCallbacks */ class Document { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="name", type="string", length=255) */ private $name; /** * @var string * * @ORM\Column(name="extension", type="string", length=255) */ private $extension; /** * Many Documents have one (the same) Folder * @ORM\ManyToOne(targetEntity="Folder", inversedBy="documents") * @ORM\JoinColumn(name="document_id", referencedColumnName="id") */ private $folder; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Document */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Set extension * * @param string $extension * * @return Document */ public function setExtension($extension) { $this->extension = $extension; return $this; } /** * Get extension * * @return string */ public function getExtension() { return $this->extension; } /** * Set folder * * @param \Playground\CookiejarBundle\Entity\Folder $folder * * @return Document */ public function setFolder(\Playground\CookiejarBundle\Entity\Folder $folder = null) { $this->folder = $folder; return $this; } /** * Get folder * * @return \Playground\CookiejarBundle\Entity\Folder */ public function getFolder() { return $this->folder; } // https://openclassrooms.com/courses/developpez-votre-site-web-avec-le-framework-symfony/creer-des-formulaires-avec-symfony // Part 4.1. "Créer des formulaires avec Symfony" private $file; // Temporary store the file name private $tempFilename; public function getFile() { return $this->file; } public function setFile(UploadedFile $file = null) { $this->file = $file; // Replacing a file ? Check if we already have a file for this entity if (null !== $this->extension) { // Save file extension so we can remove it later $this->tempFilename = $this->extension; // Reset values $this->extension = null; $this->name = null; } } /** * @ORM\PrePersist() * @ORM\PreUpdate() */ public function preUpload() { // If no file is set, do nothing if (null === $this->file) { return; } // The file name is the entity's ID // We also need to store the file extension $this->extension = $this->file->guessExtension(); // And we keep the original name $this->name = $this->file->getClientOriginalName(); } /** * @ORM\PostPersist() * @ORM\PostUpdate() */ public function upload() { // If no file is set, do nothing if (null === $this->file) { return; } // A file is present, remove it if (null !== $this->tempFilename) { $oldFile = $this->getUploadRootDir().'/'.$this->id.'.'.$this->tempFilename; if (file_exists($oldFile)) { unlink($oldFile); } } // Move the file to the upload folder $this->file->move( $this->getUploadRootDir(), $this->id.'.'.$this->extension ); } /** * @ORM\PreRemove() */ public function preRemoveUpload() { // Save the name of the file we would want to remove $this->tempFilename = $this->getUploadRootDir().'/'.$this->id.'.'.$this->extension; } /** * @ORM\PostRemove() */ public function removeUpload() { // PostRemove => We no longer have the entity's ID => Use the name we saved if (file_exists($this->tempFilename)) { // Remove file unlink($this->tempFilename); } } public function getUploadDir() { // Upload directory return 'uploads/documents/'; // This means /web/uploads/documents/ } protected function getUploadRootDir() { // On retourne le chemin relatif vers l'image pour notre code PHP // Image location (PHP) return __DIR__.'/../../../../web/'.$this->getUploadDir(); } public function getUrl() { return $this->id.'.'.$this->extension; } }
So at this point we have:
- A Folder Entity class which is the owing side of a @OneToMany relationship with a Document Entity
- A Document Entity which handles file uploads and removals using Doctrine Events
Part 2 : Forms
The Document Form is pretty straightforward since adding a document is basically having and making use of an input file button. So our form contains only a FileType field.
<?php ... use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Constraints\File; class DocumentType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('file', FileType::class, array( 'label' => false, 'required' => true, 'constraints' => array( new File(), ), )); } ... }
The Folder form type, on the other hand, needs to handle adding multiple Documents to a Folder object. This is achieved by using a CollectionType Field. The official Symfony documentation states that the Collection Type Field “is used to render a ‘collection’ of some field or form”.
<?php //... use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; class FolderType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('documents', CollectionType::class, array( 'entry_type' => DocumentType::class, 'prototype' => true, 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, 'required' => false, 'label' => false, )); //... } //... }
The field type is a CollectionType, since we need to manage a collection (an array) of documents for each folder object
- The ‘entry_type’ is a DocumentType, since our collection contains Documents
- The ‘prototype‘ option goes hand in hand with the ‘allow_add‘ option. When ‘prototype‘ is set to true, Symfony will generate a special “prototype” attribute that will be available in the form. This prototype is used to generate and render form parts which represent Document objects. When submitting the form, all these Document objects will be added to the Documents collection of the Folder object (thanks to the ‘allow_add‘ option which is set to true).
- The ‘allow_delete‘ option is the equivalent of the ‘allow_add‘, but for object removal. All the items that are not contained in the submitted data, will be removed from the main object.
- The ‘by_reference‘ option assures that the setter of the Documents field is called in all cases. We need this since the owning side of our bidirectional relationship is the Folder object. If the setter (addDocument in our case) is not called, the inverse side of the relationship is not updates and the newly created Documents are not added to the Folder object.
- The rest of the options are pretty common :
- ‘required‘ is set to false, since we allow the creation of a folder without any documents
- ‘label‘ is set to false, since we want to style that one ourselves in the template
Now that our forms are set, let’s move on to the controllers.
Part 3 : Controller and Routing
This is going to be the easiest part. We only need a Folder Controller, since we are only manipulating Folder objects. And three routes: one for adding Folders, one for editing them and a last one for viewing them.
cookiejar_folder_add: path: /cookieja/folder/add defaults: _controller: PlaygroundCookiejarBundle:Folder:add cookiejar_folder_edit: path: /cookieja/folder/{id}/edit defaults: _controller: PlaygroundCookiejarBundle:Folder:edit requirements: id: \d+ cookiejar_folder_view: path: /cookieja/folder/{id}/view defaults: _controller: PlaygroundCookiejarBundle:Folder:view requirements: id: \d+
<?php /* -------------------------------------------------------------------------------- src/Playground/CookiejarBundle/Controller/FolderController.php -------------------------------------------------------------------------------- */ namespace Playground\CookiejarBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Playground\CookiejarBundle\Entity; use Playground\CookiejarBundle\Entity\Folder; use Playground\CookiejarBundle\Form\FolderType; class FolderController extends Controller { protected $entityManager; protected $translator; protected $repository; // Set up all necessary variable protected function initialise() { $this->entityManager = $this->getDoctrine()->getManager(); $this->repository = $this->entityManager->getRepository('PlaygroundCookiejarBundle:Folder'); $this->translator = $this->get('translator'); } public function addAction(Request $request) { // Set up required variables $this->initialise(); // New object $folder = new Folder(); // Build the form $form = $this->get('form.factory')->create(FolderType::class, $folder); if ($request->isMethod('POST')) { $form->handleRequest($request); // Check form data is valid if ($form->isValid()) { // Save data to database $this->entityManager->persist($folder); $this->entityManager->flush(); // Inform user $flashBag = $this->translator->trans('folder_add_success', array(), 'flash'); $request->getSession()->getFlashBag()->add('notice', $flashBag); // Redirect to view page return $this->redirectToRoute('cookiejar_folder_view', array( 'id' => $folder->getId(), )); } } // If we are here it means that either // - request is GET (user has just landed on the page and has not filled the form) // - request is POST (form has invalid data) return $this->render( '@CookiejarBundle/Folder/add.html.twig', array ( 'form' => $form->createView(), ) ); } public function editAction(Request $request, Folder $folder) { // Set up required variables $this->initialise(); // Build the form $form = $this->get('form.factory')->create(FolderType::class, $folder); if ($request->isMethod('POST')) { $form->handleRequest($request); // Check form data is valid if ($form->isValid()) { // Save data to database $this->entityManager->persist($folder); $this->entityManager->flush(); // Inform user $flashBag = $this->translator->trans('folder_edit_success', array(), 'flash'); $request->getSession()->getFlashBag()->add('notice', $flashBag); // Redirect to view page return $this->redirectToRoute('cookiejar_folder_view', array( 'id' => $folder->getId(), )); } } // If we are here it means that either // - request is GET (user has just landed on the page and has not filled the form) // - request is POST (form has invalid data) return $this->render( '@CookiejarBundle/Folder/edit.html.twig', array ( 'form' => $form->createView(), 'folder' => $folder ) ); } }<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Nothing special going on here.
The addAction function (line 31) creates a new Folder object and assigns to a new FolderType form object (line 40). If the submitted form is valid, the data is committed to the database (lines 49, 50). The editAction function (line 73) does exactly the same thing, with the exception that the Folder object to be modified is fetched from the database.
Part 4 : Templates
Part 4.a. Creating a Folder
{{ form_start(form) }} {{ form_errors(form) }} {# The Folder.name field #} <div class="row form-group"> {{ form_label(form.name, null, {'label_attr': {'class': 'col-md-2'}}) }} <div class="col col-md-10">{{ form_widget(form.name) }}</div> <div class="form_errors col-md-12"> {{ form_errors(form.name) }} </div> </div> {# The Prototype to use for generating items of the Folder.documents collection #} <div id="filesProto" data-prototype="{{ form_widget(form.documents.vars.prototype)|e }}"></div> {# An empty (fro now) div which will later hold the generated document fields #} <div class="row"> <div class="col col-xs-12" id="filesBox"> </div> </div> {# Submit part #} <div class="row form-group"> <div class="form_cancel col-md-offset-8 col-md-2 col-xs-4"> <a href="{{ path('cookiejar_homepage') }}" class="btn btn-default btn-block">{{ 'standard_cancel' | trans }}</a> </div> <div class="form_submit col-md-2 col-xs-8"> <input type="submit" value="{{ 'standard_validate' | trans }}" class="btn btn-warning btn-block" /> </div> </div> {{ form_end(form) }}
The interesting part in this template is the following line :
<div id="filesProto" data-prototype="{{ form_widget(form.documents.vars.prototype)|e }}"></div>
If we inspect the source of the page and look for that particular div, we can see some like this :
The div contains a prototype of a Document field. In order to generate the actual Document fields, all we have to do is fetch the prototype, replace the __name__ part with an unique value, and dynamically insert it into the page. Here is the code that does that, using JQuery :
// Keep count of how many elements have been added var fileCount = '{{ form.documents|length }}'; function createAddFile(fileCount) { // grab the prototype template var newWidget = $("#filesProto").attr('data-prototype'); // replace the "__name__" used in the id and name of the prototype newWidget = newWidget.replace(/__name__/g, fileCount); $("#filesBox").append("<div class='row'>" + "<div class='col-md-10'>" + newWidget + "</div></div>"); // Once the file is added $('#playground_cookiejarbundle_folder_documents_' + fileCount + '_file').on('change', function() { // Create another instance of add file button and company createAddFile(parseInt(fileCount)+1); }); } $(document).ready(function(){ createAddFile(fileCount); fileCount++; });
The createAddFile function first grabs the prototype of the Document field and replaces the __name__ part with the value of the fileCount variable. This variable actually counts how many Documents have been rendered. When creating a new Folder, its initial value in zero. But then we edit a Folder which already contains some Documents, the value of the fileCount variable needs to correspond to the number of those Documents. Hence its initialisation : var fileCount = '{{ form.documents|length }}');
Next, the Document field is inserted into the page (line 11).
And finally, we recursively call the same function in order to generate another Document upload field.
When the page has loaded, we call the createAddFile function once, to generate the first Document upload field.
So now we have a page which allows uploading as many Documents as we want when creating a Folder. Click here to see an animation of the result.
Next, we need to add a remove button for each individual file. In order to do that, we create a removeFile function which simply removes the Document field from the form (as we saw in the Form section, the ‘allow_add‘ option will only add those fields which are present in the form at submission). Here is the modified code (the modifications are highlighted):
var fileCount = '{{ form.documents|length }}'; var removeButton = "<button type='button' class='btn btn-danger btn-xs' onclick='removeFile($(this));'><i class='fa fa-times' aria-hidden='true'></i></button>"; function removeFile(ob) { ob.parent().parent().remove(); } function createAddFile(fileCount) { // grab the prototype template var newWidget = $("#filesProto").attr('data-prototype'); // replace the "__name__" used in the id and name of the prototype newWidget = newWidget.replace(/__name__/g, fileCount); $("#filesBox").append("<div class='row'>" + "<div class='col-md-1'>" + removeButton + "</div><div class='col-md-10'>" + newWidget + "</div></div>"); // Once the file is added $('#playground_cookiejarbundle_folder_documents_' + fileCount + '_file').on('change', function() { // Create another instance of add file button and company createAddFile(parseInt(fileCount)+1); }); } $(document).ready(function(){ createAddFile(fileCount); fileCount++; });
Click here to see an animation of the result.
Before moving on to editing a Folder and its Documents, let’s style all this a little.
What we are doing is hiding the upload button for each Document, and replacing it with a fancy button which will trigger the click event of the original upload button.
// On click => Simulate file behaviour $("#jsBtnUpload" + fileCount).on('click', function(e){ $('#playground_cookiejarbundle_folder_documents_' + fileCount + '_file').trigger('click'); });
And here is the complete code:
var fileCount = '{{ form.documents|length }}'; var removeButton = "<button type='button' class='btn btn-danger btn-xs' onclick='removeFile($(this));'><i class='fa fa-times' aria-hidden='true'></i></button>"; function removeFile(ob) { ob.parent().parent().remove(); } function createAddFile(fileCount) { // grab the prototype template var newWidget = $("#filesProto").attr('data-prototype'); // replace the "__name__" used in the id and name of the prototype newWidget = newWidget.replace(/__name__/g, fileCount); newWidget = "<div style='display:none'>" + newWidget + "</div>"; hideStuff = ""; hideStuff += "<div class='col col-xs-1' id='jsRemove" + fileCount + "' style='display: none;'>"; hideStuff += removeButton; hideStuff += "</div>"; hideStuff += "<div class='col col-xs-11' id='jsPreview" + fileCount + "'>"; hideStuff += "</div>"; hideStuff += "<div class='col col-xs-12'>"; hideStuff += "<button type='button' id='jsBtnUpload" + fileCount + "' class='btn btn-warning'>"; hideStuff += "<i class='fa fa-plus'></i> {{ 'document' | trans }}"; hideStuff += "</button>"; hideStuff += "</div>"; $("#filesBox").append("<div class='row'>" + hideStuff + newWidget + "</div>"); // On click => Simulate file behaviour $("#jsBtnUpload" + fileCount).on('click', function(e){ $('#playground_cookiejarbundle_folder_documents_' + fileCount + '_file').trigger('click'); }); // Once the file is added $('#playground_cookiejarbundle_folder_documents_' + fileCount + '_file').on('change', function() { // Show its name fileName = $(this).prop('files')[0].name; $("#jsPreview" + fileCount).append(fileName); // Hide the add file button $("#jsBtnUpload" + fileCount).hide(); // Show the remove file button $("#jsRemove" + fileCount).show(); // Create another instance of add file button and company createAddFile(parseInt(fileCount)+1); }); } $(document).ready(function(){ createAddFile(fileCount); fileCount++; });
Part 4.b. Editing a Folder and its list of Documents
Now that we have successfully created a folder and its corresponding documents, the next step is to modify it. For that, we are going to build upon the same form template. The only thing that changes is the list of the Documents that have already been added to the Folder. Since our FolderType form handles the Documents fields using a CollectionType, the existing Documents are contained in the form data. So all we need to do is add them to the page.
Remember this empty div :
<div class="row"> <div class="col col-xs-12" id="filesBox"> </div> </div>
When generating the page, we simply fill it with the Documents already contained in the Collection:
<div class="row"> <div class="col col-xs-12" id="filesBox"> {% set pos = 0 %} {% for doc in form.documents %} <div class="row"> <div class="col col-xs-1" id="jsRemove{{ pos }}" style=""> <button type="button" class="btn btn-danger" onclick="removeFile($(this));"><i class="fa fa-times" aria-hidden="true"></i></button> </div> <div class="col col-xs-11" id="jsPreview{{ pos }}">{{ doc.vars.value.name }}</div> <div style="display:none"> {{ form_widget(doc) }} </div> </div> {% set pos = pos + 1 %} {% endfor %} </div> </div>
We could have simply written
<div class="row"> <div class="col col-xs-12" id="filesBox"> {% set pos = 0 %} {{ form_widget(doc) }} {% endfor %} </div> </div>
But we want the old Documents’ lines to resemble to the new ones we will be adding. So we are reproducing here the code which is usually generated by the createAddFile function.
When loading the edit page for a Folder containing Documents, the filesBox div is filled with the form fields of those documents. The fileCount variable is initialised with the total number of Documents available. When we add a new Document, it is added to the collection thanks to the ‘allow_add‘ option in the form. When we remove a Document (existing or new) we are actually removing its field from the form. Since the ‘allow_remove‘ option is set to true, this will remove the actual object from the collection. The orphanRemoval parameter in the Folder Entity class makes sure that the Documents which have been removed from the collection of a Folder are also removed from the database.
So here it is, a fully functional, flexible, multiple file upload which takes a maximum advantage of Events and CollectionTypes.
Part 5 : The Final Code
Part 6 : Further improvements
… are, of course, possible.
- [Security] File type and size checks should be done on both the client and the server side
- [Comfort] When uploading images, a preview should be displayed to the user
- [Functionality] Deleting a Folder and its Documents
Thank you for this helpful code. I want to know how can I add a description for each file during the multiple file’s uploads ?
Hello Jean,
Simply add a ‘description’ field to your DocumentType entity and include it in the DocumentFormType also. The prototype will thus contain both the name of the document and its description (the newWidget in the JS). Then you might need some JavaScript tweaking to render this filed visible while keeping the upload button hidden. But once you manage to make your input field visible, simply editing it will save the description along with the document data.
Hi Jean!
You can add text field to DocumentType and have file with description embedded form
thank you for your advice I am going to implement that right away.
Thank you for this important article, its very helpful for me.
I did all of what you describe in this article and adapt it for my project, but in final i have a button {{ ‘document’ | trans }} that do nothing. I don’t know where is the problem, but when i test just the basic form without delete button, i have an error with the var fileCount = ‘{{ form.documents|length }}’ the problem that the caractere “|” is not accepted.
The | character cannot be the problem, since it is a valid twig instruction. Are you using it in a .html.twig file as described in the article ?
Yes. My need is to have multiple images for one product, i replace Folder by Product, and Document by Image, and i save the js script in a file and add it as script in my template base.html.twig, and my template new.html.twig that add new product inherite from base.html.twig.
Good day. I am trying to set this but keep getting empty file objects, I mean I get actual objects but they are empty. Please take a look https://stackoverflow.com/questions/51727341/symfony-collection-returns-empty-files
Hello Dima,
Have you also implemented the file upload mechanisms in the Entity handling the files ?
In my example, I handle the file uploads with ORM Event (see the Document class entity above). When persist a Folder object, all related documents are handled also. If you don’t map the file objects however, you should do all these operations (renaming the file, saving it in the right location etc…) yourself in the controller.
I can handle all file savings etc, BUT a problem is that if I use single form file like :add(‘fileNames’, FileType::class..) it is all fine, when I use collection, the request gets empty file objects. As written in question $_FILES array is ok. So actually saving is not a problem, a problem to get file object from the request
Thanks a lot! Spend last few days to solve problem like this one. Best!
That was the easy version???!!!! Seriously.
Merci !
Please can you help me with this: Expected argument of type “AppBundle\Entity\Document”, “array” given
Same issue:/
Thank you so much! Perfect tutorial.
Hello ^^ I am happy it helped 😉
Hello, I have not the field to add a file, only cancel and validate button… Do you have a solution ?
Been messing with this a bunch and even directly copied the code from github and just changed the name to my schema, but it’s definitely not working. The query to insert into the documents table is always empty, with the exception of the folder id. Have you run into this before? This sort of magic behind the scenes stuff is almost impossible to debug to try and figure out where the error is.
Hey! It is a wonderfull tutorial, but still not working. No add-buttons, empty documents, and something goes wrong unfortunately. Could you upload complete project, where is the code working? It would be sooo fine. Thanks a lot
Okkay, forget my comment from a while ago: i tried to debug with dumping submitted variables and the _POST and request contains both: Folder and Document (just one), because add-buttons stil not working. Creating of Folders and copy Docs in there are also still not working, but i think, its my fault. Found the Project on git hub and will try again, maybe I’m too stupid for this.
Me again. Add Buttons working, but the folder creation and adding of documents are still not working – following error comes: Expected argument of type “Symfony\Component\HttpFoundation\File\UploadedFile or null”, “array” given. This coming from setFile(UploadedFile $file = null) –> how should this work?? because life is too short to spend 3 days with it, if isnt’t working well. 🙁
Hello Tatsi,
The error you are describing makes me think that maybe your input buttons are accepting multiple files. So instead of sending a single file, you are actually sending an array containing a single file.
When submitting your form, you can check the Network activity in your browser and verify the content of the post variables.
I am working on a tutorial on uploading multiple files using Symfony and Dropzone, I will let you know when I will upload it (August probably)
Hello Livia! Thanks a lot for reply 🙂 Yes, I sending an array – I misunderstanded the description of tutorial: Multiple File Upload, sorry 😀 I’m already looking forward to the new tutorial. Thank you for your work!!!
Hi again! Sorry for get on your nerves, but I have an last question: after eliminating of all previous issues, I have the same error, like JAMIE: the query to insert documents into database is always empty, all values are setted to null, but the File Object is filled with data, I unfortunatelly don’t get why 🙁 Could you help with this?? I thank you in advance!
Document {#4606 ▼
-id: null
-name: null
-extension: null
-folder: null
-file: UploadedFile {#9 ▶} // contains uploaded file
-tempFilename: null
}
Hello Tatsi,
Did you make sure that your Document Entity contains this line : @ORM\HasLifecycleCallbacks ?
Above the class Document statement.
This ensures that your @ORM\PrePersist(), @ORM\PreUpdate(), @ORM\PostPersist(), @ORM\PostUpdate() are well executed, because they are in charge of sending all the correct data to the database.
Livia! You are the best! I had already an Document Entity and copypasted just a parts from your DocumentEntity and missed this line. This line fixed this issue and NOW EVERYTHING WORKING FINE!!! Thank you very much! 1000 hugs 🙂 Now I hope a can extend the code with the physical creation of folders in uploads, with Filesystem Component…
Hello Tatsi,
I am so happy it worked 😉
I have some tutorials in progress that will show how to create a real cloud file system. But it will take a while to write 😀
I will send you a message when new tutorials will be live.
Hello Livia!!! And I’m so happy it works!!!! More than happy! I finally discovered that the folders are also physically created, not just persisted in the database, in the wrong place, but I am correcting that. Filesystem Component dont needed anymore. I expected some action or command like mkdir, and thought at first -> the folders does not really created, but searched for them in the wrong place … omg … Thanks again and I am looking forward for new tutorials made by you , Livia!
Tatsi- symfonynoob
Hello, I found your article very helpful, but I have one question, how could you handle File constraint ? For example, if app supports only jpeg file and that user send png file ?
Hello
You just add your constraints in the Form handling the File Type. In this article that would be DocumentType.php
$builder->add(‘file’, FileType::class, array(
‘required’ => false,
‘label’ => false,
‘constraints’ => array(
new File([
‘mimeTypes’ => $options[‘mimeTypes’],
‘maxSize’ => $options[‘maxSize’],
‘maxSizeMessage’ => $options[‘maxSizeMessage’],
]),
),));
In this example, my $options are :
$options[‘mimeTypes’] =
(
[0] => image/jpeg
[1] => image/jpeg
[2] => image/png
)
$options[‘maxSize’] = 512M
$options[‘maxSizeMessage’] = “Fichier trop volumineux”
Remember to add all the proper includes in your form file :
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Form\Extension\Core\Type\FileType;
I hope this helps ^^
Thanks for your reply. I was not clear enough my problem is how handle error in twig. My constraints are working but I don’t figure out how display error after form submit. If I add 2 files that are not supported (png and max size for example ) and 1 valid how inform user for each file the associate error/success
images help to understand what the problem is
First step : https://ibb.co/27vQWDR
After submit : https://ibb.co/hg3DC9z
Seems logic form is invalid so no file is uploaded and preview of valid-image.jpeg is not available.