Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Fetching the User In a Service

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

We 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 Security 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.

Leave a comment!

16
Login or Register to join the conversation
Ad F. Avatar

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 :(

5 Reply

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!

Reply

Thx Ryan,
But how to test service who inject Security dependence ?
ty

Reply

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!

Reply
Default user avatar
Default user avatar muureke | posted 3 years ago

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?

Reply

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!

Reply
Default user avatar
Default user avatar muureke | weaverryan | posted 3 years ago | edited

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.

Reply

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!

Reply
Default user avatar

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.

Reply

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!

Reply
Default user avatar

Ryan, your persistence, dedication, and erudition are magnificent and generous. Thank you!

1 Reply

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?

Reply

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!

1 Reply

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?

Reply

I know you already know this :) but just in case someone else reads it. Check the TIP on this episode: https://symfonycasts.com/sc...

1 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.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
    }
}
userVoice