Custom User Methods & 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 fetch the current user object in a controller. What about from Twig? Head to base.html.twig. Let's see... this is where we render our "log out" and "log in" links. Let's try to render the first name of the user right here.

App.user In Twig

How? In Twig, we have access to a single global variable called app, which has lots of useful stuff on it, like app.session and app.request. It also has app.user which will be the current User object or null. So we can say app.user.firstName:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ app.user.firstName }}
... line 41
{% else %}
... lines 43 - 44
{% endif %}
</div>
</div>
</nav>
... lines 49 - 53
</body>
</html>

This is safe because we're inside of the is_granted() check... so we know there's a User.

Let's try it! Close the profiler, refresh the page and... perfect! Apparently my name is Tremayne!

Now that we've got this... time to make it fancier. Inside of the is_granted() check, I'm going to paste in a big user menu: you can get this from the code block on this page:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
class="dropdown-toggle btn"
type="button"
id="user-dropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<img
src="https://ui-avatars.com/api/?name=John+Doe&size=32&background=random"
alt="John Doe Avatar">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="#">Log Out</a>
</li>
</ul>
</div>
{{ app.user.firstName }}
<a class="nav-link text-black-50" href="{{ path('app_logout') }}">Log Out</a>
{% else %}
... lines 61 - 62
{% endif %}
</div>
</div>
</nav>
... lines 67 - 71
</body>
</html>

This is completely hard-coded to start... but it renders nicely!

Let's make it dynamic... there are a few spots. For the image, I'm using an avatar API. We just need to take out the "John Doe" part and print the user's real first name: app.user.firstName. Oh, then pipe that into |url_encode so it's safe to put in a URL. Also render app.user.firstName inside the alt text:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
src="https://ui-avatars.com/api/?name={{ app.user.firstName|url_encode }}&size=32&background=random"
alt="{{ app.user.firstName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

For the "log out" link, steal the path() function from below... and put it here:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
... lines 41 - 51
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="{{ path('app_logout') }}">Log Out</a>
</li>
</ul>
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Delete the old stuff at the bottom to finish this up:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
class="dropdown-toggle btn"
type="button"
id="user-dropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<img
src="https://ui-avatars.com/api/?name={{ app.user.firstName|url_encode }}&size=32&background=random"
alt="{{ app.user.firstName }} Avatar">
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown">
<li>
<a class="dropdown-item" href="{{ path('app_logout') }}">Log Out</a>
</li>
</ul>
</div>
{% else %}
... lines 59 - 60
{% endif %}
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

Sweet! When we refresh.. voilà! A real user drop-down menu.

Adding Custom Methods to User

I've mentioned a few times that our User class is our class.... so we are free to add whatever methods we want to it. For example, imagine that we need to get the user's avatar URL in a few places on our site... and we don't want to repeat this long string.

Copy this and then go open the User class: src/Entity/User.php. All the way at the bottom, create a new public function getAvatarUri(). Give this an int $size argument that defaults to 32... and a string return type:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
... lines 158 - 162
}
}

Paste the URL as an example. Let's return the first part of that... add a ? - which I totally just forgot - then use http_build_query():

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
... lines 159 - 161
]);
}
}

Pass this an array... with the first query parameter we need: name set to $this->getFirstName().

Oh, but we can be even smarter. If you scroll up, the firstName property is allowed to be null:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 31
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
... lines 36 - 163
}

It's an optional thing that a user can provide. So, back down in the method, use getFirstName() if it has a value... else fallback to the user's email. For size, which is the second query parameter, set it to $size... and we also need background set to random to make the images more fun:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getFirstName() ?: $this->getEmail(),
'size' => $size,
'background' => 'random',
]);
}
}

Thanks to this nice little method, back in base.html.twig we can replace all of this with app.user.avatarUri:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
src="{{ app.user.avatarUri }}"
... line 50
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

You can also say getAvatarUri(): both will do the same thing.

If we try it... broken image! Ryan: go add the ? you forgot, you knucklehead. http_build_query adds the & between the query parameters, but we still need the first ?:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getFirstName() ?: $this->getEmail(),
... lines 160 - 161
]);
}
}

Now... much better!

But we can make this even better-er! In base.html.twig, we're using app.user.firstName:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
... line 49
alt="{{ app.user.firstName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

As we just saw, this might be empty. So let's add one more helper method to User called getDisplayName(), which will return a string:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 164
public function getDisplayName(): string
{
... line 167
}
}

I'll steal some logic from above... and return that:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 164
public function getDisplayName(): string
{
return $this->getFirstName() ?: $this->getEmail();
}
}

So we either return the first name or the email. We can use this up in getAvatarUri() - getDisplayName():

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 155
public function getAvatarUri(int $size = 32): string
{
return 'https://ui-avatars.com/api/?' . http_build_query([
'name' => $this->getDisplayName(),
... lines 160 - 161
]);
}
... lines 164 - 168
}

And also in base.html.twig:

... line 1
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<div class="dropdown">
<button
... lines 42 - 46
>
<img
... line 49
alt="{{ app.user.displayName }} Avatar">
</button>
... lines 52 - 56
</div>
{% else %}
... lines 59 - 61
</div>
</div>
</nav>
... lines 65 - 69
</body>
</html>

When we refresh... yup! It still works!

Security Service: Fetching the User in a Service

Ok: we have now fetched the User object from a controller via $this->getUser()... and in Twig via app.user. The only other place where you'll need to fetch the User object is from within a service.

For example, a couple of tutorials ago, we created this MarkdownHelper service:

... lines 1 - 8
class MarkdownHelper
{
... lines 11 - 23
public function parse(string $source): string
{
if (stripos($source, 'cat') !== false) {
$this->logger->info('Meow!');
}
if ($this->isDebug) {
return $this->markdownParser->transformMarkdown($source);
}
return $this->cache->get('markdown_'.md5($source), function() use ($source) {
return $this->markdownParser->transformMarkdown($source);
});
}
}

We pass it markdown, it converts that into HTML... and then... profit... or something. Let's pretend that we need the User object inside of this method: we're going to use it log another message.

If you need the currently authenticated User object from a service, you can get it via another service called Security. Add a new argument type-hinted with Security - the one from Symfony\Component - called $security. Hit Alt + Enter and go to "Initialize properties" to create that property and set it:

... lines 1 - 6
use Symfony\Component\Security\Core\Security;
... lines 8 - 9
class MarkdownHelper
{
... lines 12 - 17
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache, bool $isDebug, LoggerInterface $mdLogger, Security $security)
{
... lines 20 - 23
$this->security = $security;
}
... lines 26 - 46
}

Because I'm using PHP 7.4, this added a type to my property.

Down below, let's log a message if the user is logged in. To do this, say if $this->security->getUser():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
if (stripos($source, 'cat') !== false) {
$this->logger->info('Meow!');
}
if ($this->security->getUser()) {
... lines 34 - 36
}
... lines 38 - 45
}
}

Really, this is the way to fetch the User object... but we can also use it to see if the User is logged in because this will return null if they're not. A more "official" way to do this would be to use isGranted() - that's another method on the Security class - and check for IS_AUTHENTICATED_REMEMBERED:

class MarkdownHelper
{
    // ...
    public function parse(string $source): string
    {
        // ...
        if ($this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
            // ...
        }
        // ...
    }
}

Anyways, inside say $this->logger->info() with:

Rendering markdown for {user}

Pass a context array with user set to $this->security->getUser()->getEmail():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
... lines 29 - 32
if ($this->security->getUser()) {
$this->logger->info('Rendering markdown for {user}', [
'user' => $this->security->getUser()->getUserIdentifier()
]);
}
... lines 38 - 45
}
}

Like before, we know this will to be our User object... but our editor only knows that it's some UserInterface. So we could use getEmail()... but I'll stick with getUserIdentifier():

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 26
public function parse(string $source): string
{
... lines 29 - 32
if ($this->security->getUser()) {
$this->logger->info('Rendering markdown for {user}', [
'user' => $this->security->getUser()->getUserIdentifier()
]);
}
... lines 38 - 45
}
}

Let's try it! We have markdown on this page... so refresh... then click any link on the web debug toolbar to jump into the profiler. Go to logs and... got it! There are a bunch of logs because we call this method a bunch of times.

Next, let's talk about a super useful feature called "role hierarchy". This gives you the power to assign extra roles to any user that has some other role.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}