Buy
Buy

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(string $size = null): string
{
$url = 'https://robohash.org/'.$this->getEmail();
if ($size)
$url .= sprintf('?size=%dx%d', $size, $size);
return $url;
}
}

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!

  • 2019-03-11 weaverryan

    Hey Cryptoblob!

    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!

  • 2019-03-11 Cryptoblob

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

  • 2018-11-05 Diego Aguiar

    Thanks for sharing it!

  • 2018-11-02 Jeroen van den Nieuwenhuisen

    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.