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 SubscribeOnce you have your authentication system step, pff, life is easy! On a day-to-day basis, you'll spend most of your time in a controller where... well, there's really only two things you can do related to security. One, deny access, like, based on a role:
... lines 1 - 8 | |
/** | |
* @IsGranted("ROLE_USER") | |
*/ | |
class AccountController extends AbstractController | |
{ | |
... lines 14 - 22 | |
} |
Or two, figure out who is logged in.
That's exactly what we need to do in AccountController
so that we can start printing out details about the user's account. So... how can we find out who is logged in? With $this->getUser()
:
... lines 1 - 11 | |
class AccountController extends AbstractController | |
{ | |
... lines 14 - 16 | |
public function index() | |
{ | |
dd($this->getUser()->getFirstName()); | |
... lines 20 - 22 | |
} | |
} |
Go back to your browser and head to /account
. Nice! This gives us the User
entity object! That's awesome because we can do all kinds of cool stuff with it. For example, let's see if we can log the email address of who is logged in.
Add a LoggerInterface $logger
argument:
... lines 1 - 4 | |
use Psr\Log\LoggerInterface; | |
... lines 6 - 12 | |
class AccountController extends AbstractController | |
{ | |
... lines 15 - 17 | |
public function index(LoggerInterface $logger) | |
{ | |
... lines 20 - 23 | |
} | |
} |
Then say $logger->debug()
:
Checking account page for
And then $this->getUser()
. Because we know this is our User
entity, we know that we can call, getEmail()
on it. Do that: ->getEmail()
:
... lines 1 - 12 | |
class AccountController extends AbstractController | |
{ | |
... lines 15 - 17 | |
public function index(LoggerInterface $logger) | |
{ | |
$logger->debug('Checking account page for '.$this->getUser()->getEmail()); | |
... lines 21 - 23 | |
} | |
} |
Cool! Move over and refresh. No errors. Click anywhere down on the web debug toolbar to get into the profiler. Go to the logs tab, click "Debug" and... down a bit, there it is!
Checking account page for
spacebar5@example.com
.
$this->getUser()
But, hmm, something is bothering me: I do not get any auto-complete on this getEmail()
method. Why not? Hold Command or Control and click the getUser()
method. Ah: it's simple: Symfony doesn't know what our User
class is. So, its PhpDoc can't really tell PhpStorm what this method will return.
To get around this, I like to create my own BaseController
class. In the Controller/
directory, create a new PHP class called BaseController
. I'll make it abstract
because this is not going to be a real controller - just a helpful base class. Make it extend the normal AbstractController
that we've been using in our existing controllers:
... lines 1 - 2 | |
namespace App\Controller; | |
... lines 4 - 5 | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
abstract class BaseController extends AbstractController | |
{ | |
... lines 10 - 13 | |
} |
Tip
A simpler solution (and one that avoids a deprecation warning) is to advertise to your IDE that getUser() returns a User (or null) with some PHPDoc:
/**
* @method User|null getUser()
*/
class BaseController extends AbstractController
{
}
Then, I'll go to the "Code"->"Generate" menu - or Command
+N
on a Mac, click "Override Methods" and override getUser()
. We're not actually going to override how this method works. Just return parent::getUser()
. But, add a return type User
- our User
class:
... lines 1 - 4 | |
use App\Entity\User; | |
... lines 6 - 7 | |
abstract class BaseController extends AbstractController | |
{ | |
protected function getUser(): User | |
{ | |
return parent::getUser(); | |
} | |
} |
From now on, instead of extending AbstractController
, we should extend BaseController
:
... lines 1 - 11 | |
class AccountController extends BaseController | |
{ | |
... lines 14 - 23 | |
} |
And this will give us the proper auto-completion on getUser()
:
... lines 1 - 11 | |
class AccountController extends BaseController | |
{ | |
... lines 14 - 16 | |
public function index(LoggerInterface $logger) | |
{ | |
$logger->debug('Checking account page for '.$this->getUser()->getEmail()); | |
... lines 20 - 22 | |
} | |
} |
I also like to use my BaseController
to add other shortcut methods specific to my app. If there's something that you do frequently, but it doesn't make sense to move that logic into a service, just add a new protected function
.
I won't go and update my other controllers to extend BaseController
right this second - I'll do that little-by-little when I need to.
Ok: we now know how to fetch the User
object in a controller. So, how can we fetch it inside a template? Find the templates/
directory and open our account/index.html.twig
. The answer is... app.user
. That's it! We can call app.user.firstName
:
{% extends 'base.html.twig' %} | |
{% block title %}Manage Account!{% endblock %} | |
{% block body %} | |
<h1>Manage Your Account {{ app.user.firstName }}</h1> | |
{% endblock %} |
Try that out. Go back to /account
and... perfect!
Symfony gives you exactly one global variable in Twig: app
. And it just has a few helpful things on it, like app.user
and app.session
. And because app.user
returns our User
object, we can call firstName
on it. Twig will call getFirstName()
on User
.
Oh, and, oof. This page is super ugly. Clear out the h1
. I'm going to paste in some HTML markup I prepared: you can copy this markup from the code block on this page:
{% extends 'base.html.twig' %} | |
{% block title %}Manage Account!{% endblock %} | |
... lines 4 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row user-menu-container square"> | |
<div class="col-md-12 user-details"> | |
<div class="row spacepurplebg white"> | |
<div class="col-md-2 no-pad"> | |
<div class="user-image"> | |
<img src="https://robohash.org/hello@symfonycasts.com" class="img-responsive thumbnail"> | |
</div> | |
</div> | |
<div class="col-md-10 no-pad"> | |
<div class="user-pad"> | |
<h3>Welcome back, ?????</h3> | |
<h4 class="white"><i class="fa fa-twitter"></i> ?????</h4> | |
<a class="btn btn-labeled btn-info" href="#"> | |
<span class="btn-label"><i class="fa fa-pencil"></i></span>Update | |
</a> | |
</div> | |
</div> | |
</div> | |
<div class="row overview"> | |
<div class="col-md-4 user-pad text-center"> | |
<h3>COMMENTS</h3> | |
<h4>184</h4> | |
</div> | |
<div class="col-md-4 user-pad text-center"> | |
<h3>ARTICLES READ</h3> | |
<h4>1,910</h4> | |
</div> | |
<div class="col-md-4 user-pad text-center"> | |
<h3>LIKES</h3> | |
<h4>3,892</h4> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
If you refresh right now... oof. It still looks pretty terrible. Oh, hello robot! Anyways, the page looks awful because this markup requires another CSS file. If you downloaded the course code, you should have a tutorial/
directory. We already copied this login.css
file earlier. Now, copy account.css
, find your public/
directory, open css/
and... paste! To include this stylesheet on this page, add block stylesheets
and endblock
:
... lines 1 - 4 | |
{% block stylesheets %} | |
... lines 6 - 8 | |
{% endblock %} | |
... lines 10 - 49 |
Inside, call parent()
so that we add to the existing stylesheets, instead of replacing them. Add link
and point to css/account.css
:
... lines 1 - 4 | |
{% block stylesheets %} | |
{{ parent() }} | |
<link rel="stylesheet" href="{{ asset('css/account.css') }}"> | |
{% endblock %} | |
... lines 10 - 49 |
PhpStorm auto-completes the asset()
function for me.
Now refresh again. So much better! All of this markup is 100% hardcoded. But I added friendly ?
marks where we need to print some dynamic stuff. Let's do it! For the Avatar, we're using this cool RoboHash site where you give it an email, and it gives you a robot avatar. I love the Internet!
Replace this with app.user.email
:
... lines 1 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row user-menu-container square"> | |
<div class="col-md-12 user-details"> | |
<div class="row spacepurplebg white"> | |
<div class="col-md-2 no-pad"> | |
<div class="user-image"> | |
<img src="https://robohash.org/{{ app.user.email }}" class="img-responsive thumbnail"> | |
</div> | |
</div> | |
... lines 21 - 29 | |
</div> | |
... lines 31 - 44 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Then, down by "Welcome back", replace that with app.user.firstName
:
... lines 1 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row user-menu-container square"> | |
<div class="col-md-12 user-details"> | |
<div class="row spacepurplebg white"> | |
<div class="col-md-2 no-pad"> | |
<div class="user-image"> | |
<img src="https://robohash.org/{{ app.user.email }}" class="img-responsive thumbnail"> | |
</div> | |
</div> | |
<div class="col-md-10 no-pad"> | |
<div class="user-pad"> | |
<h3>Welcome back, {{ app.user.firstName }}</h3> | |
... lines 24 - 27 | |
</div> | |
</div> | |
</div> | |
... lines 31 - 44 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Cool! Let's see how it looks like now.
Hey! A brand new robot avatar and we see the first name of the dummy user. We are still missing this twitter handle... because... our User
class doesn't have that property yet:
... lines 1 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row user-menu-container square"> | |
<div class="col-md-12 user-details"> | |
<div class="row spacepurplebg white"> | |
... lines 16 - 20 | |
<div class="col-md-10 no-pad"> | |
<div class="user-pad"> | |
... line 23 | |
<h4 class="white"><i class="fa fa-twitter"></i> ?????</h4> | |
... lines 25 - 27 | |
</div> | |
</div> | |
</div> | |
... lines 31 - 44 | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Let's add that next. Add a cool shortcut method to our User
class and talk about how we can fetch the User object from the one place we haven't talked about yet - services.
Hey Stephane
Which Symfony version are you using?
In Symfony4 AbstractController
is not final, but we don't extend from it anyways, we extend from Controller.php
. If you keep seeing that message is because you are overriding the getUser()
method when you should not.
Cheers!
Hey MolloKhan,
Thank for your reply.
I use Symfony 4.1.5. I have only follow the tip of Ryan about BaseController.
Oh yes, you can extend from the "AbstractController" class, it's totally ok to do that (and probably it's better). About the deprecation message, I just tried it and yes, I do see that message too but I'm not totally sure if we can just ignore it. I'll ask Ryan about it but it may take some time to get an answer back.
Cheers!
Hey MolloKhan!
Great catch! I didn't realize that these methods had been marked as final! I talked internally about it, and here is a better solution:
// ....
use App\Entity\User;
// ...
/**
* @method User getUser()
*/
class BaseController extends AbstractController
{
}
Basically, you don't need to override getUser() in order to "advertise" that it returns some other object. You can instead use this nice @method
trick. You get the same result, but no deprecation.
Thanks again for the report!
it is me again
what i mean with my first comment ( what if i want to fetch the user when building a form i mean in buildForm function ) is that if i want my form to be constructed dynamicaly depending on data from database
Hey EarlyBird,
I suppose you're talking about this comment https://symfonycasts.com/sc... - do you want to inject the current logged in user in your form type? Am I correct? Well, you can still use that solution I said in the first comment - pass the current user as option to the form type. I suppose you create those dynamic form types in a controller, so you can easily fetch the current user with $this->getUser() in the controller.
Otherwise, use dependency injection to inject the Symfony\Component\Security\Core\Security into your form type, and you should be able to fetch the current user via $this->security->getUser(); inside.
I hope this helps!
Cheers!
Hey EarlyBird!
Yes, you can do it. You can pass any options you want to the buildForm() method, see the 2nd argument called $options. It depends on your form, if your form is based for User object, you can do it out-of-the-box calling $options['data']. Otherwise, you can pass any data you want as options when you create your form type.
Cheers!
Hello,
I still don't get it. Where does the $this->getUser() function comes from ? How can i get user in separete controller ?
Hey @wuwu!
I like that you're wondering what $this->getUser()
really does :). Check out this chapter: https://symfonycasts.com/screencast/symfony-security/user-in-service
Cheers!
Instead of using `getUser()` shortcut method, we can use dependency injection: `UserInterface` type-hint in arguments of controller action.
```
/**
* @Route("/something")
* @IsGranted("IS_AUTHENTICATED_REMEMBERED")
*/
public function create(UserInterface $user): Response
{
```
Unfortunately, this is currently buggy in combination with @IsGranted when user is logged out. Now, to fix that it's needed to use nullable type-hint `?UserInterface`. I created pull request to fix it inside annotations bundle: https://github.com/sensiola...
Hey Tomasz,
Thank for this tip! Btw, if you can share a link to the docs where you found this feature - would be awesome! Yeah, it makes sense to use "?UserInterface" type-hint on those pages where user might be anonymous, good point! Otherwise, you will have problems. Or, probably "create(UserInterface $user = null)" might work too, but I have not tried it yet. But anyway, "?UserInterface" makes more sense here.
But this is interesting that it's buggy in combination with isGranted(). Are you sure it's related to injecting the current user? If you fo not inject the current user - isGranted() starts working well again? Because, at the first sight, injecting the current user and isGranted() annotation has nothing in common except using current logged in user from the internals. Though I'm do not know the internal code that's responsible for annotations.
Btw, what about using "$this->isGranted('IS_AUTHENTICATED_REMEMBERED')" in the beginning of your create() method instead of @isGranded annotation? Does it works well or also have problems?
Cheers!
It seems to me there is no information about this feature in the documentation. There is blog post about it: https://symfony.com/blog/ne...
There is a bug in combination of @IsGranted (specifically annotation, no AbstractController shortcut) and UserInterface type hint without nullable. It makes sense to use UserInterface type hint without nullable and without default "null" value if @IsGranted enforces user session. Technical details about bug are described here: https://github.com/sensiola... Basically @IsGranted is interpreted AFTER preparation of controller arguments, this causes problem. My pull request tries to fix it: https://github.com/sensiola...
Hey TomaszGasior!
Yea, one of the biggest reason that it's not documented (or at least, poorly documented) is that I don't really like this feature (though other people definitely disagree with me!). We already have a lot of magic in the controller, and I didn't see the benefit of adding yet-another "magic" resolution of controller arguments. Also, because you have to type-hint UserInterface, your editor doesn't will only auto-complete methods on that interface - not any custom methods you have. That's also true with the getUser() shortcut, but you can work around that by adding documentation to a base controller (like we do in this tutorial). This is probably also why that bug exists - not a lot of people use this way of getting the user.
So, I hope that at least explains a bit about the missing documentation and the bug. But, let me know!
Cheers!
I think "getUser()" in BaseController should return "?User", because if nobody is logged in, the method will return null. And thanks for showing "https://robohash.org/" - it's hilarious! 🤖
// 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.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"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.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}
Hello,
I notice that symfony profiler generate a log message about deprecation :
The "Symfony\Bundle\FrameworkBundle\Controller\AbstractController::getUser()" method is considered final. It may change without further notice as of its next major version. You should not extend it from "App\Controller\BaseController".
It is not a problem ?