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 SubscribeOur 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!
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.
You should not write your robohash-url in your entity, please use a view-helper instead, god damn it :-p
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!
{{ 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?
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!
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
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!
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 }}
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!
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?
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!
By starting > v5, FontAwesome starting tag is "fab" but not "fa". :)
Please, fix it inside the html.twig files :)
https://fontawesome.com/ico...
Regards.
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!
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!
// 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
}
}
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.