Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

A bit of Security Cleanup

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 $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

There's one last piece of business that we need to clean up. In ArticleAdminController, we created this endpoint... but we didn't add any security on it! It's open entirely to the world: there's no @IsGranted annotation above the method or above the class.

Securing the new Endpoint

Now... this might be ok - this endpoint just returns some boring, non-sensitive HTML anyways. But, let's be cautious.

The tricky thing is that this endpoint is used on both the new and edit form pages. On the new page, we require you to have ROLE_ADMIN_ARTICLE. But on the edit page, we use a special voter that gives you access if you have ROLE_ADMIN_ARTICLE or if you are the author of the article.

So, hmm - our endpoint needs to be available to anyone that has ROLE_ADMIN_ARTICLE or is the author of at least one article. A little odd, but we can make that happen!

The proper way to solve this is to create a new voter and call @IsGranted() with a new attribute we invent, like ADMIN_ARTICLE_FORM. The voter would handle that attribute and have all the logic inside.

But... because we only need to use this security logic on this one endpoint, and because I'm feeling lazy, let's instead put the logic right in the controller. We can always move it to a voter later if we need to re-use it.

First, add @IsGranted("ROLE_USER") to at least make sure the user is logged in. Then, inside the method, if not $this->isGranted('ROLE_ADMIN_ARTICLE') and $this->getUser()->getArticles() === 0, then we should not have access. Wait, but the ->getArticles() method is not auto-completing for me.

... lines 1 - 14
class ArticleAdminController extends BaseController
{
... lines 17 - 69
/**
... line 71
* @IsGranted("ROLE_USER")
*/
public function getSpecificLocationSelect(Request $request)
{
// a custom security check
if (!$this->isGranted('ROLE_ADMIN_ARTICLE') && $this->getUser()->getArticles()->isEmpty()) {
... lines 78 - 92
}
... lines 94 - 105
}

Oh, I know why! Go to the top of this class and change the base class from extends AbstractController to extends BaseController.

... lines 1 - 14
class ArticleAdminController extends BaseController
... lines 16 - 107

Reminder: BaseController is a controller that we created. It extends AbstractController but it adds a return type to getUser() with our User class so we get auto-completion.

Back down in our method, we can say $this->getUser()->getArticles()->isEmpty(), which is a method on Doctrine's Collection object. So, if we don't have ROLE_ADMIN_ARTICLE and we are not the author of any articles, throw $this->createAccessDeniedException().

... lines 1 - 73
public function getSpecificLocationSelect(Request $request)
{
... line 76
if (!$this->isGranted('ROLE_ADMIN_ARTICLE') && $this->getUser()->getArticles()->isEmpty()) {
throw $this->createAccessDeniedException();
}
... lines 80 - 92
}
... lines 94 - 107

Done! And just to make sure I didn't completely break things, if I change the location to "Near a star"... yea! It still loads.

Fetch EXTRA_LAZY

What really made adding this security easy was being able to call $this->getUser()->getArticles(). The problem is that if this user is the author of 200 articles, then this will query for 200 rows of articles and hydrate those into 200 full objects, just to figure out that, yes, we are the author of at least one article. All we really need is a quick count query of the articles.

Fortunately, we can tell isEmpty() to do that! Open User and look for that articles property. At the end of the @OneToMany annotation, add fetch="EXTRA_LAZY". We talked about this option in our Doctrine relations tutorial. With this set, if we simply try to count the articles - which is what isEmpty() does - then Doctrine will make a quick COUNT query instead of fetching all the data. Nice!

... lines 1 - 19
class User implements UserInterface
{
... lines 22 - 63
/**
* @ORM\OneToMany(targetEntity="App\Entity\Article", mappedBy="author", fetch="EXTRA_LAZY")
*/
private $articles;
... lines 68 - 268
}

Using the @method in BaseController

Ok, one more thing - and it's also unrelated to forms. Open BaseController. By extending AbstractController, this class gives us all the great shortcut method we love but it also overrides getUser() so that our editor knows that this method will return our specific User class.

After we did this, a wonderful SymfonyCasts user pointed out that the getUser() method on the parent class is marked as final with @final. When something is final it means that we are not allowed to override it. Symfony could enforce this by changing the method to be final protected function getUser(). Then, we would get an error! But, Symfony often uses the softer @final comment, which is just documentation, either to prevent breaking backward compatibility or because it's harder for Symfony to unit test code that has final methods.

Anyways, the method is intended to be final, which means that we're not supposed to override it. So, delete the method in our class. There's another nice solution anyways: above the class add @method User getUser().

... lines 1 - 7
/**
* @method User getUser()
*/
abstract class BaseController extends AbstractController
... lines 12 - 13

That's it! That does the exact same thing: it hints to our IDE that the getUser() method returns our User object. Back in ArticleAdminController, if we delete getArticles() and re-type... yep! It works!

Phew! Amazing job people! That was a huge topic to get through. Seriously, congrats!

The Symfony form system is both massively powerful and, in some places, quite complex. It has the power to make you incredibly productive or just as unproductive if you use it in the wrong place or the wrong ways. So, be smart: and follow these two rules.

One: if your form looks quite different than your entity, either remove the data_class option and use the associative array the form gives you to do your work or bind your form to a model class. Two: if your form has a complex frontend with a lot of AJAX and updating, it might be easier - and a better user experience - if you skip the form and write everything with JavaScript. Use this tool in the right places, and you'll be happy.

Let me know what you guys are building! And, as always, if you have any questions, ask us down in the comments.

Alright friends, see ya next time.

Leave a comment!

40
Login or Register to join the conversation
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | posted 4 years ago

the problem I see that a user may have ROLE_ADMIN_ARTICLE and may not have written any article, and it's the first time, or have I missed something? By the way, thank you for the isEmpty() function, didn't know the power of collections until I was given to look for that function, lol => https://www.doctrine-projec...

3 Reply

Hey Jose carlos C.

the problem I see that a user may have ROLE_ADMIN_ARTICLE and may not have written any article, and it's the first time
There should not be a problem because if the User has the ROLE_ADMIN_ARTICLE role, then the IF statement is valid because we check for that specific role OR if he's the author of at least one article. I hope it makes any sense to you.

Cheers!

Reply
Twan V. Avatar
Twan V. Avatar Twan V. | posted 3 years ago

Could you prehabs add a chapter about multi-page forms?

1 Reply

Hey Twan!

We won't cover that in this tutorial - but we might in a future Symfony 5 tutorial :). But, here are some tips I can give you:

A) First, there is a bundle to help with this! I highly recommend checking it out: https://github.com/craue/CraueFormFlowBundle

B) If that doesn't work for you, then basically the "trick" of multi-page forms is that, after submitting the first form, you need to save that data somewhere. Sometimes this is easy. For example, suppose you are building some "Business company" via 3 pages of forms. If you are ok with saving the data to some BusinessCompany entity after step 1 (which would mean that you would "kind of" have an row in the database with incomplete data) then you're good! Save the data to that entity after step 1, then save more data on step 2 and more data on step 3. But if you don't want to save the data to the database until it's complete, you'll need to store the data somewhere. One easy option is the "session". Basically, after successfully submitting step 1, you would save the data to the session. Do the same after step 2. Finally after finishing, use all the session data to create the entity and save it.

Let me know how it goes!

Cheers!

Reply
Parian Avatar

Thank you for this great tutorial that allows me to better understand the form system and not to be afraid of it anymore!

However I would have loved to have another tutorial to go a bit further in forms to see what Symfony (with a bit of JavaScript) can do with embedded forms.

Maybe it will be cover in a future tutorial !

Reply

Hey Parian

First of all thanks for the feedback! I registered your course request, we will discuss it internally =)

As for me embedded form are very complex and hard to maintain, so I'd prefer to avoid them until they are really need! However it's still easier to build some sort of JS app to manipulate relations without embedding another form =)

Cheers!

Reply
Parian Avatar
Parian Avatar Parian | sadikoff | posted 1 year ago | edited

Thank you @vsadikoff for your reply and for considering my request.

I know that the management of forms with Symfony has limitations. Besides, at work we use directly an API with React to manage all the frontend.

But from a personal point of view, I would like to create a small and simple project without using a JS framework. But even with a simple project, we can quickly have "complex" needs in terms of user interface. That's why I was thinking about an advanced tutorial on forms to go to the end of form management just with symfony and light javascript.

I know there are plenty of resources on the net, but Symfony Cast are really excellent and very professional!

Reply

Yeah I got you point of view! Stay tuned and keep watching tutorials, probably one day we will have it in our list!

Cheers!

Reply

Since I already earned the stimulus tutorial certificate I implemented both the autocomplete field and the dependent select fields using stimulus and it works pretty nice :)

I hope when (if) you update this tutorial to Symfony 5 you are going to use stimulus for the JS stuff.

Thanks a lot for another great tutorial :)

Reply

Hey elkuku

Thanks for sharing your ideas of what you would like to see in future tutorials, we really take into account people's opinion :)
And yes, we will deliver more Symfony5 tutorials

Cheers!

Reply

Good night. I wish I could add an editor in the textarea fields. What would be your recommendation? Thank you very much for your great work. I don't speak English but I love listening to the tutorials. Thanks again.

Reply

Hey GerMonty

It's nice to know that you liked our site. I haven't use this JS library but it seems promising https://editorjs.io/

Cheers!

Reply

Thanks for the recommendation. I'm going to try it.

1 Reply

Thank you for this amazing guide! I've went through your tutorials in the following order:

1) Stellar Development with Symfony 4
2) Symfony 4 Fundamentals: Services, Config & Environments
3) Doctrine & the Database
4) Mastering Doctrine Relations!
5) Symfony Security: Beautiful Authentication, Powerful Authorization
6) Symfony 4 Forms: Build, Render & Conquer!

and I feel dangerous enough to start my own application. I'm tired of keeping all of my passwords in a gigantic spreadsheet, totally insecure and horrible to manage. So I would like to code my own management tool for that. Your tutorials gave me pretty much all the basics that I need to accomplish that. But for now, I have one real and one personal question:

1) Which courses come after this? I enjoyed watching these in the "correct" order by release date, so I could finish each of these courses without changing the project. Does this series continue with The Messenger, the Symfony Mailer, Uploading files or creating Symfony bundles? As a general hint I took a look at the Symfony version on the bottom right of your screen to keep track of that.

2) I eventough I feel dangerous enough to start my application, I don't feel particularly professional enough to actually do it. This is my personal question: When do I stop, or, should I stop trying to study as much as I can and get to work? Like, if I started my own project from the very beginning as soon as course 1, I would've ended up recoding and refactoring SO many things OVER and OVER again, making me regret even starting it things the "easy" way. Now I know a few more efficient ways to do things and I would like to happily implement these new things, that I have learned, in my new project. But now that I have read a little about the Messenger and uploading files and sending emails and everything, I feel like I should study those courses aswell, before starting. And I have this feeling that I'm not going to stop until I've learned everything. Or at least I think that, but in reality that's obviously not going to happen.
Do you have any thoughts on this matter? Or general tips? Why am I feeling so anxious / pressured about this? I'm writing this hoping you know what I mean (and feel). I've been doing very superficial HTML/CSS/PHP work the past few years and haven't done anything, where I could say "hey, this is some pretty deep coding stuff", hence my self-doubt. Sure I got the job done, but I always felt kind of "dirty" about it.

Thanks for coming to my Ted talk!

Reply

Hi denizgelion!

Nice to chat with you :). And congrats on finishing all the courses - I realize it takes a lot of time investment. You won't regret it! About your questions, let me see if i can help!

> 1) Which courses come after this? I enjoyed watching these in the "correct" order by release date, so I could finish each of these courses without changing the project.

You've done things perfectly so far. And the truth is, after the courses that you've already watched, there really is no "correct order". You now have all the fundamentals, so the remaining tutorials are all about "I want to learn how to send emails" or "I want to learn Messenger". You have the ability to do any of them in any order - and your main motivation should be: "What do I *need* to learn to accomplish my task".

> 2) I eventough I feel dangerous enough to start my application, I don't feel particularly professional enough to actually do it. This is my personal question: When do I stop, or, should I stop trying to study as much as I can and get to work? Like, if I started my own project from the very beginning as soon as course 1, I would've ended up recoding and refactoring SO many things OVER and OVER again, making me regret even starting it things the "easy" way.

That's a good, tough question. If your goal is to learn (and it sounds like it is!) then there are two things I'd say:

A) Coding through a real project (even if it's just a hobby project) is *the* best way to learn things and find where your knowledge is missing.

B) If your purpose is to learn, then you must be ok with "wasting your time". Well, to say it nice, don't worry too much about "am I doing things wrong?". You need to dive in and say "If I do things wrong today, and learn a better way tomorrow, that's ok! I will be better because of my practice". Even if you *had* started coding your project after just one tutorial (only to discover better/easier ways of doing things), your practice coding on your real project will help you learn things more *profoundly*. Things are no longer theoretical - you will start to have "Ah ha! That would have been a much easier way to accomplish that task I did in my project".

So, "learning by studying" and "learning through practice" are a partnership - each compliments the other one and neither is effective in isolation. Do both, and know that by mixing them, you're getting close to the "optimal place" :). I know it's hard sometimes because you feel you may make some mistakes. But the reality is, you *need* to make those mistakes to keep moving forward - embrace them! My bet is that, after you start coding your real project, you'll return to tutorials you've watched with new questions - probably some situation where your use-case is not *quite* like the tutorial, and you're trying to figure out how to adapt things. When that happens, you can always ask us in the comments.

Let me know if this helps! And keep up the good work :). I guess that ends MY Ted Talk :D

Cheers!

1 Reply

I appreciate you taking your time for a lengthy answer like this, I really do. Since I'm at my own, there's no confirmation coming from anywhere, that I'm doing things "right" or "wrong".

> So, "learning by studying" and "learning through practice" are a partnership - each compliments the other one and neither is effective in isolation. Do both, and know that by mixing them, you're getting close to the "optimal place"

Deep inside I knew this was the answer, but I guess I was looking for confirmation. So I appreciate you telling me, that I'm at least on the right track. It means alot!
Thanks for helping out- I will recommened your tutorials to anyone I know. But for now, I guess I'll be going to start *the* project and probably will be back when my "use-case is not *quite* like the tutorial" ;)

Greetings from Germany!

Reply

Hey denizgelion!

I'm happy this was helpful - working in isolation is especially hard when you're learning. I look forward to seeing you back in the comments.

Good luck!

Reply

Just a little clarification about the annotation.
You have to add the possibility to have null (user not authenticated), otherwise PHPStan won't be happy ;)
`
/**

  • @method User|null getUser()
    */
    `

Big thanks for your work!

Reply

Hey John,

Thanks for this tip! Yes, you're right, there should be "User|null". Unfortunately, I can't change it in code because we want our code matches the video, but you can totally tweak it in *your* code.

Cheers!

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | posted 3 years ago | edited

Hello, there
I think it's not really great to forbid to see options for people without articles, so if I write my fist article ? :)

Reply

Hey Alexandr,

Haha, that "if" condition is tricky, right? :) Probably not too obvious at the first sight, but if users has "ROLE_ADMIN_ARTICLE" role - we will NOT *deny* them access. We *deny* access in that "if" instead of *granting*, so it should work if you're an author and want to create the first article :) But sure, if you don't have "ROLE_ADMIN_ARTICLE" role, i.e. you're not an author, and want to write your first article - you can't! To do so you're *required* to have that "ROLE_ADMIN_ARTICLE" role. So, that's our business logic, you can have your own of course :)

Cheers!

1 Reply
william-bridge Avatar
william-bridge Avatar william-bridge | posted 3 years ago

Hi Sir very great tutorial.
I have a question about this situation : i have an order system (for example in e-commerce) with several steps (3 steps for example). Each step (relativ to one page) have a form but a form in the second page depend on the form in the first page. Usually i'm using session to store data of the form in the first page for using it in the next page but it is generally complicated for me to manage it like that with session (build a form with a data of other form).
So please Sir what is the best way to manage several forms where certains depends on each other ?

Reply

Hey william-bridge

I think what you can do is to create a DTO (data transfer object) per each step of your form, or maybe just one big DTO for the whole form (depends on how well you want to organize your information and how complex it's your form), then, you bind such class to your form, passing the object as second argument of the $this->createForm() call (so, the form gets pre-filled), and finally you store in the session such object whenever there is a submit for going to the next step. I hope this helps you out :)

Cheers!

Reply
Benoit L. Avatar
Benoit L. Avatar Benoit L. | posted 4 years ago

Hello for point 1, I am not sure, I know the model class, but what is the associative array for the form?

Reply

Hey Benoit L.

Sorry but I didn't get your question. Could you elaborate it a bit more so I can help you out?

Cheers!

Reply
Benoit L. Avatar

In the video at 6min38, the slide lists 2 points, "either remove the data_class...and use the associative array"

Reply

Ahh I get it know. When you don't link a class to a form, then when you call $form->getData(); what you will get is an object which implements the ArrayAccess interface, so you can use it as an array. So, if you FormType has a field named "email", you can access to it like this


$data = $form->getData();
$email = $data['email'];

I hope it makes any sense to you. Cheers!

Reply
Benoit L. Avatar

Got it thank you !

1 Reply
Adsasd Avatar

Awesome tutorial, thank you Ryan & Team!
Can you elaborate on the 2nd option you're mentioning at the end of this video?
How could JS replace the SF forms system in the backend? We used it only on the font end so far?
Any hints? Thank you in advance :)

Reply

Hey Adsasd

There are many front-end solutions for building forms or you can just create your own, for example, if you are already using ReactJS, you may want to create your own form and manage it with React. So, in theory, you can develop a back-end application on Symfony and delegate all the HTML, forms, styles, etc to a front-end framework

I hope this makes any sense to you :)
Cheers!

Reply
Vasiliy Z. Avatar
Vasiliy Z. Avatar Vasiliy Z. | posted 4 years ago

Outstanding tutorial.
When you start customizing textarea, hoped that you continue with it and connect some wysiwyg editor, but you don`t, that was sad to me ))

Reply

Hey Vasiliy Z.!

I tried to more-or-less cover that topic here: https://symfonycasts.com/sc...

Because, ultimately, turning a field into a WYSIWYG editor is the same two-step process:

1) In your form, give your field some class that you can use in JavaScript (e.g. js-wysiwyg)

2) Write some JavaScript that finds fields with those classes, and initializes whatever WYSIWYG JavaScript library on those fields.

And... that's it! To Symfony, this look know different, because the field submits the same as before.

I hope this helps!

Cheers!

Reply
Christoph Avatar
Christoph Avatar Christoph | posted 4 years ago

Great tutorial :-)
Thanks a lot for that :-)

I have one more question.
I want to build a menu from database.
It should be loaded additionally in the template.

How do i manage that? Is there a tutorial for that?

Thanks

Reply

Hey Chris,

Unfortunately, we don't have a tutorial about it, though you can our tutorial about Symfony Forms (if you haven't watched it yet): https://symfonycasts.com/sc... . Also, I'd recommend you to take a look at KnpMenuBundle that helps to work with menus: https://github.com/KnpLabs/... . I think with some intermediate integration layer you can make Symfony Forms and KnpMenuBundle do what you're looking for.

I hope this helps!

Cheers!

Reply
Adam M. Avatar
Adam M. Avatar Adam M. | posted 4 years ago

Great tutorial start to finish!
I've learnt heaps from this.

Reply
Abdelaziz M. Avatar
Abdelaziz M. Avatar Abdelaziz M. | posted 4 years ago

Good and nice topic i never seen it before,since 2014 i used symfony.
Thanks a lot.

Reply

You are welcome! :)

Reply
Default user avatar

Hi Ryan Weaver!

I Symfony 3 series there where two tutorials "Journey to the Center of Symfony" namely "HttpKernel Request-Response" and "The Dependency Injection Container". Those were the huge ones - the real heart of Symfony. Do you consider analogous tutorials in Symfony 4 series?

P.S. What's next in your timetable after Forms?

Reply

Hey Matt!

Yea, really think that we should do those tutorials for Symfony 4 as well - I LOVED those tutorials. The problem right now (a good problem) is that there's just so much good stuff to cover. We'll start a phpspec tutorial in about a week (not too big), then I'm not entirely sure - we'll be making plans in about a week. But we're targeting API Platform in 2019, Messenger, probably more JavaScript stuff, design patterns stuff, maybe this idea you had, and more. We're going to go as fast as we can :).

Cheers!

4 Reply
Mouad E. Avatar
Mouad E. Avatar Mouad E. | weaverryan | posted 4 years ago | edited

happy to hear that weaverryan thank you very much

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "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.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice