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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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.
If your DropDown and Other Bootstrap Functionality doesn’t work, Do try this:
Inside <b>config > packages > twig.yaml</b> add
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
// start the Stimulus application
import './bootstrap';
// activates collapse functionality
import { Collapse } from 'bootstrap';
// Need jQuery?
import $ from 'jquery';
/**
*/
`
Then Execute following commands in your Terminal: