Setup for Uploading Private Article 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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNew challenge folks! Our alien authors are begging for a new feature: they want to be able to upload "supporting" files and attach them to the article - like PDFs that they're referencing, images... text notes... really anything. But these files will only be visible to anyone that can edit an article. I'll call these "article references" and every article will be able to have zero to many references, which is where things start to get interesting.
Creating the ArticleReference Entity
Let's create the new entity:
php bin/console make:entity
Call it ArticleReference
and give it an article
property. This will be a relation
back to the Article
class. This will be a ManyToOne relation: each Article
can have many ArticleReferences. Then, this will be not null in the database: every ArticleReference
must be related to an Article
. Say yes to map the other side of the relationship - it's convenient to be able to say $article->getArticleReferences()
. And no
to orphan removal - we won't be using that feature.
Nice! Ok, this needs a few more fields: filename
a string that will hold the filename on the filesystem, originalFilename
, a string that will hold the original filename that was on the user's system - more on that later - and mimeType
- we'll use that to store what type of file it is - which will come in handy later.
And... done! Next run:
php bin/console make:migration
Let's go make sure the migration file doesn't contain any surprises... yep!
CREATE TABLE
article_reference
... with a foreign key back to article
. Run that with:
php bin/console doctrine:migrations:migrate
Removing Extra Adder/Remover
Before we get back to work, open the Article
entity. The command did create the $articleReferences
property that allows us to say $article->getArticleReferences()
. That's super convenient. It also added addArticleReference()
and removeArticleReference()
. I'm going to delete these: I'm just not going to need them: I'll read the references from the article, but never set them from this direction.
// ... lines 1 - 18 | |
class Article | |
{ | |
// ... lines 21 - 89 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\ArticleReference", mappedBy="article") | |
*/ | |
private $articleReferences; | |
// ... line 94 | |
public function __construct() | |
{ | |
// ... lines 97 - 98 | |
$this->articleReferences = new ArrayCollection(); | |
} | |
// ... lines 101 - 315 | |
/** | |
* @return Collection|ArticleReference[] | |
*/ | |
public function getArticleReferences(): Collection | |
{ | |
return $this->articleReferences; | |
} | |
} |
Form CollectionType
Ok team: let's think about how we want this to work. The user needs to be able to upload multiple reference files to each article. A lot of you may be expecting me to use Symfony's CollectionType
: that's a special field that allows you to embed a collection of fields into a form - like multiple upload fields.
Well... sorry. We are definitely not going to use CollectionType
. That field is hard enough to work with if you want to be able to add or delete rows. Adding uploading to that? Oof, that's crazy talk.
We're going to do something different. And it's going to be a much better user experience anyways! We're going to leave the main form alone and build a separate "article reference upload", sort of, "widget", next to it that'll eventually upload via AJAX, allow deleting, editing and re-ordering. It's gonna be schweet!
Adding the HTML Form
Open the edit template: templates/article_admin/edit.html.twig
. Everything we're going to do will be inside of this template, not the new template. The reason is simple: trying to upload files to a new entity - something that hasn't been saved to the database - is super hard! You need to store files in a temporary spot, keep track of them, and assign them to the entity when your user does finally save - if they ever do that. So, totally possible - but complex. If you can, have your user fill in some basic data, save your new entity to the database, then show the upload fields.
Anyways, let's add an <hr>
and set up a bit of structure: div class="row"
and div class="col-sm-8"
. Say "Details" here and move the entire form inside. Now add a div class="col-sm-4"
and say "References".
// ... lines 1 - 2 | |
{% block content_body %} | |
<h1>Edit the Article! ?</h1> | |
<hr> | |
<div class="row"> | |
<div class="col-sm-8"> | |
<h3>Details</h3> | |
{{ include('article_admin/_form.html.twig', { | |
button_text: 'Update!' | |
}) }} | |
</div> | |
<div class="col-sm-4"> | |
<h3>References</h3> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 20 - 26 |
Let's see how this looks... nice! Form on the left, upload widget thingy on the right.
Here's the plan: add a <form>
tag with the normal method="POST"
and enctype="multipart/form-data"
. Inside, add a single upload field: <input type="file" name="">
, how about reference
. Then, <button type="submit">
, some classes to make it not ugly, and "Upload".
// ... lines 1 - 2 | |
{% block content_body %} | |
// ... lines 4 - 7 | |
<div class="row"> | |
// ... lines 9 - 14 | |
<div class="col-sm-4"> | |
<h3>References</h3> | |
<form action="" method="POST" enctype="multipart/form-data"> | |
<input type="file" name="reference"> | |
<button type="submit" class="btn btn-sm btn-primary">Upload</button> | |
</form> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 25 - 31 |
Cool! Yes, we are going to talk about allowing the user to upload multiple files at once. Don't worry, things are going to get much fancier.
Next, let's get the endpoint setup for this upload and store everything in the database, including a few pieces of information about the file that we did not store for the article images.
Argh! I would have liked to have tackled the CollectionType! What do you recommend if we really have to use them?
For example, we have a page that allows us to create multiple users at once. And to each user, we have to attach a file (an ID card, an avatar, whatever).
We are therefore forced to use a CollectionType. What is really annoying in this case is that in the controller, we will have to loop through each user to add the associated file.
Worse, if we are in another even less cool context, in which we have a collection with each item which itself contains a collection of files (and which can in the future be modified, deleted, added). It's an ordeal!
Therefore, I was wondering, would it be possible, a bit like with VichUploader, that we create an event that would take care of all the management of the files, alone, without having to do it ourselves? from the controller?
With VichUploader, when we submit a form that contains a file, VichUploader alone manages the addition of the file. Could we not do the same?
EDIT: I found this (old) tutorial from Grafikart (a boss) : https://grafikart.fr/tutori... .It looks pretty good as a mode of operation! Do you think we could use it?