Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom User Method

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

Our fancy new account page is complete! Oh, except for that missing Twitter username part - aliens freakin' love Twitter. The problem is that we don't have this field in our User class yet. No problem, find your terminal and run:

php bin/console make:entity

to update the User entity. Add twitterUsername... and make this nullable in the database: this is an optional field:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 39
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $twitterUsername;
... lines 44 - 134
public function getTwitterUsername(): ?string
{
return $this->twitterUsername;
}
public function setTwitterUsername(?string $twitterUsername): self
{
$this->twitterUsername = $twitterUsername;
return $this;
}
}

Cool! Now run:

php bin/console make:migration

Let's go check that out: look in the Migrations/ directory and open the new file:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20180831195803 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE user ADD twitter_username VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE user DROP twitter_username');
}
}

And... yep! It looks perfect. Move back to your terminal one more time and run:

php bin/console doctrine:migrations:migrate

Excellent! Now that we have the new field, let's set it on our dummy users in the database. Open UserFixture. Inside the first set of users, add if $this->faker->boolean, then $user->setTwitterUsername($this->faker->userName):

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
... lines 21 - 24
if ($this->faker->boolean) {
$user->setTwitterUsername($this->faker->userName);
}
... lines 28 - 34
});
... lines 36 - 51
}
}

The $faker->boolean is cool: it will return true or false randomly. So, about half of our users will have a twitter username.

Go reload! Run:

php bin/console doctrine:fixtures:load

Finally! Let's get to work in account/index.html.twig. Replace the ? marks with app.user.twitterUsername:

... 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">
... lines 23 - 24
<h4 class="white"><i class="fa fa-twitter"></i> {{ app.user.twitterUsername }}</h4>
... lines 26 - 29
</div>
</div>
</div>
... lines 33 - 46
</div>
</div>
</div>
{% endblock %}

Hmm, but we probably don't want to show this block if they don't have a twitterUsername. No problem: surround this with an if statement:

... 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
{% if app.user.twitterUsername %}
<h4 class="white"><i class="fa fa-twitter"></i> {{ app.user.twitterUsername }}</h4>
{% endif %}
... lines 27 - 29
</div>
</div>
</div>
... lines 33 - 46
</div>
</div>
</div>
{% endblock %}

Perfect! Ok, let's go find a user that has their twitterUsername set! Run:

php bin/console doctrine:query:sql "SELECT * FROM user"

Scroll up and... cool: spacebar1@example.com. Move back to your browser and refresh. Oh! We got logged out! That's because the id of the user that we were logged in as was removed from the database when we reloaded the fixtures.

Login as spacebar1@example.com, password engage. Click sign in and... got it!

Custom User Method for RoboHash

Oh, and there's one other thing that we can finally update! See the user avatar on the drop-down? That's totally hardcoded. Let's roboticize that! Yea, roboticize apparently is a real word.

Copy the src for the RoboHash:

... 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 - 31
</div>
... lines 33 - 46
</div>
</div>
</div>
{% endblock %}

Then, open up base.html.twig and, instead of pointing to the astronaut's profile image, paste it!

... 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="https://robohash.org/{{ app.user.email }}">
</a>
... lines 41 - 47
</li>
... lines 49 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

Try it! Move over and... refresh!

Nice! But, hmm... there is one small thing that I don't like. Right click on the image, copy the image address and paste in a new tab. Oh. That's a pretty big image: 300x300. It's not a huge deal, but our users are downloading a pretty big image, just to display this teenie-tiny thumbnail.

Fortunately, the fine people who created RoboHash added a feature to help us! By adding ?size=100x100, we can get a smaller image. Let's do that on the menu.

But, wait! Instead of just putting ?size= right here... let's get organized! I don't like duplicating the RoboHash link everywhere. Open your User class. Let's add a new custom function called public function getAvatarUrl().

We don't actually have an avatarUrl property... but that's ok! Give this an int argument that's optional and the method will return a string:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 146
public function getAvatarUrl(string $size = null): string
{
... lines 149 - 154
}
}

Inside, set $url = and paste the RoboHash link. Remove the email but add $this->getEmail():

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 146
public function getAvatarUrl(string $size = null): string
{
$url = 'https://robohash.org/'.$this->getEmail();
... lines 150 - 154
}
}

Easy enough! For the size part, if a $size is passed in, use $url .= to add sprintf('?size=%dx%d'), passing $size for both of these wildcards. At the bottom, return $url:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 146
public function getAvatarUrl(int $size = null): string
{
$url = 'https://robohash.org/'.$this->getEmail();
if ($size) {
$url .= sprintf('?size=%dx%d', $size, $size);
}
return $url;
}
... lines 157 - 158

Now that we're done with our fancy new function, go into index.html.twig, remove the long string, and just print app.user.avatarUrl:

... 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="{{ app.user.avatarUrl }}" class="img-responsive thumbnail">
</div>
</div>
... lines 21 - 31
</div>
... lines 33 - 46
</div>
</div>
</div>
{% endblock %}

We can reference avatarUrl like a property, but behind the scenes, we know that Twig is smart enough to call the getAvatarUrl() method.

Copy that, go back into base.html.twig and paste. But this time, call it like a function: pass 100:

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

Let's see if it works! Close a tab then, refresh! Yep! And if we copy the image address again and load it... nice! A little bit smaller.

Next, let's find out how to fetch the user object from the one spot we haven't talked about yet: services.

Leave a comment!

19
Login or Register to join the conversation
Jeroen V. Avatar
Jeroen V. Avatar Jeroen V. | posted 4 years ago | edited

Instead of $uri .= sprintf('?size=%dx%d', $size, $size); you can do $uri .= sprintf('?size=%1$dx%1$d', $size);. This removes the duplication of the $size arguments.

2 Reply

Thanks for sharing it!

Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | posted 2 years ago

You should not write your robohash-url in your entity, please use a view-helper instead, god damn it :-p

Reply

Hey Szabolcs!

That's definitely a valid way to handle it :). Because I don't need any service logic, I can use the simpler solution and put it in the entity. But it *is* true that if, later for some reason, we need to refactor this logic to require a service, we'll need to do more work (update our templates) to make that change. So better organization now would make that possible situation easier later.

Cheers!

Reply
Default user avatar
Default user avatar name123 | posted 2 years ago | edited

public function getAvatarUrl(string $size = null)

I think you meant int here, not string

Reply

You're right! That's our fault. I'm gonna fix it right now :)
Thanks for letting us know!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

{{ app.user.username }}
Where does .user come from? You might have explained it, but I can't remember. Is it coming from the User class?
Is it guessing wether .user matches the User class?

Reply

Hey Farry7,

It looks like you've missed some information following this course :). That "app" is a special global Twig variable, it's an object of AppVariable class, that has some useful methods, one of them is "getUser()". But Twig is smart enough to call getter methods behind the scene when you call just {{ app.user }}, i.e. behind the scene Twig will call that getUser method that will return the current logged in user or null. Basically, you can manually specify that method like {{ app.getUser() }}, but usually everybody shorts that call to just {{ app.user }{.

Hope this is clear for you now.

Cheers!

Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 3 years ago | edited

Ok, so what you can do is to check both configurations and look if something relevant is different. If you run php -i in your terminal you will see all PHP settings

Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 3 years ago | edited

Hey @disqus_uUZdE4XYyx

Does it work when you run your application through the browser? If you only got that error via a console command it's very likely that it's running a different php executable.

Cheers!

Reply
Duilio P. Avatar
Duilio P. Avatar Duilio P. | posted 4 years ago | edited

I don't know if this is a good idea or not, but I ended up passing the user to the view, in this way I've got both autocompletion in PHPStorm and a shorter way to get the user data:


return $this->render('account/index.html.twig', [
    'user' => $this->getUser()
]);

In the view:

{{ user.firstName }}

Reply

Hey Duilio P.

The only downside I see to your approach is that you could pass a different User object to your template and it would look like as the logged in user. About autocompletion, I have the same problem, I believe the Symfony plugin has a bug on it.

Cheers!

1 Reply
Duilio P. Avatar
Duilio P. Avatar Duilio P. | MolloKhan | posted 4 years ago | edited

Thanks, you're right :) I'll remember to add a test for that or to use app.user instead.

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 4 years ago

Well PHPStorm does autocomplete the app.user variable inside of twig, but it doesn't do it any further (it can't autocomplete/doesnt show anything beyond app.user.) is that normal?

Reply

It behaves exactly the same for me. I believe there is a way to tell PhpStorm the instance of which class is that varaible but I'm not sure how. Probably you can talk to the developers of the Symfony plugin about this.

Cheers!

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 4 years ago

By starting > v5, FontAwesome starting tag is "fab" but not "fa". :)
Please, fix it inside the html.twig files :)

https://fontawesome.com/ico...

Regards.

Reply

Hey Abelardo L.!

You're right! Well, we're using FontAwesome 4 in this tutorial - that's why we have the fa tag :). But if you're using Font Awesome 5, then you're totally right: fab fa-twitter.

Thanks!

1 Reply
Tom Avatar

In the getAvatarUrl method, why do you call `$this->getEmail()` instead of `$this->email`?

Reply

Hey Tom!

No particular reason. In general, I tend to like to *always* call my getters, in the (rare) event that I add some sort of logic in that method that changes the return value in some way I want, or maybe throws an exception if something isn't right. It just gives me that flexibility if I need it, which is honestly rare in these very simple entity classes. For me, both are fine :).

Cheers!

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