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 SubscribeWe know how to get the user in a template:
... line 1 | |
<html lang="en"> | |
... lines 3 - 15 | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5"> | |
... lines 18 - 21 | |
<div class="collapse navbar-collapse" id="navbarNavDropdown"> | |
... lines 23 - 34 | |
<ul class="navbar-nav ml-auto"> | |
{% if is_granted('ROLE_USER') %} | |
<li class="nav-item dropdown" style="margin-right: 75px;"> | |
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | |
<img class="nav-profile-img rounded-circle" src="{{ app.user.avatarUrl(100) }}"> | |
</a> | |
... lines 41 - 47 | |
</li> | |
... lines 49 - 52 | |
{% endif %} | |
</ul> | |
</div> | |
</nav> | |
... lines 57 - 74 | |
</body> | |
</html> |
And... we know how to get the user from a controller with $this->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 | |
} | |
} |
But... what about from inside a service? Because this nice $this->getUser()
shortcut will only work in controllers.
To show you what I mean, I need to remind you of a feature we built a long time ago, like 3 screencasts ago. Click on any article. Then, click anywhere on the web debug toolbar to open the profiler. Find the "Logs" section and click on "Info & Errors". There it is!
They are talking about bacon again!
This is a super-informative log message that we added from inside our markdown service: src/Service/MarkdownHelper.php
:
... lines 1 - 8 | |
class MarkdownHelper | |
{ | |
... lines 11 - 23 | |
public function parse(string $source): string | |
{ | |
if (stripos($source, 'bacon') !== false) { | |
$this->logger->info('They are talking about bacon again!'); | |
} | |
// skip caching entirely in debug | |
if ($this->isDebug) { | |
return $this->markdown->transform($source); | |
} | |
$item = $this->cache->getItem('markdown_'.md5($source)); | |
if (!$item->isHit()) { | |
$item->set($this->markdown->transform($source)); | |
$this->cache->save($item); | |
} | |
return $item->get(); | |
} | |
} |
This code parses the article content through markdown and caches it. But also, if it sees the word "bacon" in the content ... which every article has in our fixtures, it logs this message.
So here's our challenge: I want to add information about who is currently logged in to this message. To do that, we need to answer one question: how can we access the current User object from inside a service?
The answer is... of course - by using another service. The name of the service that gives you access to the User
object is easy to remember. Add another argument: Security $security
:
... lines 1 - 7 | |
use Symfony\Component\Security\Core\Security; | |
class MarkdownHelper | |
{ | |
... lines 12 - 18 | |
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $markdownLogger, bool $isDebug, Security $security) | |
{ | |
... lines 21 - 25 | |
} | |
... lines 27 - 48 | |
} |
I'll hit Alt
+Enter
and click "Initialize Fields" to create that property and set it:
... lines 1 - 7 | |
use Symfony\Component\Security\Core\Security; | |
class MarkdownHelper | |
{ | |
... lines 12 - 16 | |
private $security; | |
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $markdownLogger, bool $isDebug, Security $security) | |
{ | |
... lines 21 - 24 | |
$this->security = $security; | |
} | |
... lines 27 - 48 | |
} |
So how can we use this service? Well... let's just look inside! Hold Command
or Control
and click to open the Security
class. It has just two important methods: getUser()
and isGranted()
. Hey! That makes a lot of sense! Remember, once you set up authentication, there are only two things you can do with security: get the user object or figure out whether or not the user should have access to something, like a role. That's what isGranted()
does.
Close that and move down to the log message. Ok, we could get the user object, maybe call getEmail()
on it, and concatenate that onto the end of the log string. But! There's a cooler way. Add a 2nd argument to info
: an array. Give it a user
key - I'm just making that up - and set it to the user object: $this->security->getUser()
:
... lines 1 - 9 | |
class MarkdownHelper | |
{ | |
... lines 12 - 27 | |
public function parse(string $source): string | |
{ | |
if (stripos($source, 'bacon') !== false) { | |
$this->logger->info('They are talking about bacon again!', [ | |
'user' => $this->security->getUser() | |
]); | |
} | |
... lines 35 - 47 | |
} | |
} |
Unrelated to security, every method on the logger, like info()
, debug()
or alert()
, has two arguments. The first is the message string. The second is an optional array called a "context". This is just an array of any extra info that you want to include with the log message. I invented a user
key and set it to the User
object.
Let's go see what it looks like! Refresh! Then, click back into the profiler, find logs, and check out "Info & Errors". The message looks the same, but now we have a "Show Context" link. Click that! Nice! There is our entire User
object in all of its glory. That's pretty sweet. And now, you know how to get the User
object from anywhere.
Next, we get to talk about a feature called "role hierarchy". A little feature that will make you love working with roles, especially if you have complex access rules.
Hey Ad F.
That's a good question, what you can do is create a form type specific for updating a user, in that form type you can add a "UserPassword" field type, that field type has all the logic you are looking for. You can find more detailed info about it in the docs: https://symfony.com/doc/cur...
Cheers!
Hey gponty
If you are doing an unit test, then you can just mock up that dependency but if you're doing a functional test, then you can just fetch the service from the Container and use it as normal
Cheers!
I have a method in a service which creates a `Cat` entity:
```php
private function addCat(array $catData)
{
$cat = new Cat();
$cat->setName($catData['name']);
$cat->setUser($this->security->getUser());
// etc.
}
```
But I get the error that `setUser` expects a `User` entity but is getting a `UserInterface` instead. Where am I going wrong?
Hey @muureke!
Hmm, what does the setUser() method look like. Specially, what type-hint does it have (I think it’s User) and what use statement do you have for User in the Cat class?
Cheers!
Thanks for the quick response! Set user looks like this:
`
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
`
...and there is no use statement for User in the Cat class. I haven't modified anything after the models and relationships were created for me by make:entity.
Hey muureke!
Sorry for the slow reply! Hmm, yea - that looks fine. Something weird is definitely happening. In that addCat() method, I would dd($this->security->getUser()) and week what you get. I’m thinking that , somehow, the authenticated User object is *not* an instance of your User entity. I’m not sure how that would happen yet, but that’s the next place to look :).
Cheers!
Hi Ryan,
Thanks for responding again! This is the result of my `dd`:
User^ {#1033 ▼
-id: 1
-email: "test@user.com"
-roles: []
-password: "$argon2i$v=19$m=65536,t=4,p=1$n8S2a5yloMxednTCdVNDOQ$j4a5T4PwRuc7dbE0Ayv55N906bF2trO2zjhM9AAojF4"
-cats: PersistentCollection^ {#1250 ▶}
}
So it's a user. And, er, here's where I have to confess: the code actually works. I've realised now that it's just PhpStorm giving me that error ("Expected parameter of type `\App\Entity\User`, `Symfony\Component\Security\Core\User\UserInterface` provided").
So: sorry for wasting your time. Although if you know why PhpStorm is giving this error it'd be nice to squish it.
Yo @muureke!
I have to confess: the code actually works. I've realised now that it's just PhpStorm giving me
Ah :D. No worries - I get curious about this stuff too. And I can answer this :). If you looked at the PHPDoc on the $this->security->getUser()
method (https://github.com/symfony/symfony/blob/08a218c79fdc4018b67cf9ba19e601d065ea0f04/src/Symfony/Component/Security/Core/Security.php#L38) you'll see that it returns UserInterface. The reason is that this is a core Symfony function... and the only rule for a "User" object is that it is any object that implements UserInterface. Of course in YOUR application, you know that getUser()
will return your User object, but Symfony can only know its an instance of UserInterface
.
When PhpStorm see this, it says "Hey! This method returns a UserInterface... but this other method that you're passing that value to requires a User. What's going on?". Obviously, you can ignore it... but that's the reason why ;). If you were hardcore about it, you could write code like this:
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new \Exception('User is not correct instance');
}
$cat->setUser($user);
That would technically be "more" correct... but I feel it's a bit of a waste of time - the exception would NEVER be hit in your app.
Cheers!
But how does it log all information about a User?
In markdown.log there's only this information:
[2018-10-19 13:49:37] markdown.INFO: They are talking about bacon again! {"user":"[object] (App\\Entity\\User: {})"} []
There's no specific id of the user.
Doesn't it mean that it only shows the User object of a user, who is currently logged in?
Hey boykodev
> Doesn't it mean that it only shows the User object of a user, who is currently logged in?
Well, yes, the `Security::getUser()` method returns the logged in user object or null in case you are anonymous
Were you logged in? You could dump what `Security::getUser()` returns and see what it's actually returning.
Cheers!
The "Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait::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".
What about that deprecation message, should we ignore it? Is there another way to get user autocompletion from PhpStorm?
I know you already know this :) but just in case someone else reads it. Check the TIP on this episode: https://symfonycasts.com/sc...
// 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
}
}
care to teach us how to create a form to update the user profile ?
password field is a must, therefore we cant create a form to update profile without password :(