Buy Access to Course
03.

Customizing the User Entity

Share this awesome video!

|

The really neat thing about Symfony's security system is that it doesn't care at all about what your User class looks like. As long as it implements UserInterface, so, as long as it has these methods, you can do anything you want with it. Heck, it doesn't even need to be an entity!

Adding more Fields to User

For example, we already have an email field, but I also want to be able to store the first name for each user. Cool: we can just add that field! Find your terminal and run:

php bin/console make:entity

Update the User class and add firstName as a string, length 255 - or shorter if you want - and not nullable. Done!

Check out the User class! Yep, there's the new firstName property and... at the bottom, the getter and setter methods:

118 lines | src/Entity/User.php
// ... lines 1 - 10
class User implements UserInterface
{
// ... lines 13 - 29
/**
* @ORM\Column(type="string", length=255)
*/
private $firstName;
// ... lines 34 - 105
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
}

Awesome!

Setting Doctrine's server_version

I think we're ready to make the migration. But! A word of warning. Check out the roles field on top:

118 lines | src/Entity/User.php
// ... lines 1 - 10
class User implements UserInterface
{
// ... lines 13 - 24
/**
* @ORM\Column(type="json")
*/
private $roles = [];
// ... lines 29 - 116
}

It's an array and its Doctrine type is json. This is really cool. Newer databases - like PostgreSQL and MySQL 5.7 - have a native "JSON" column type that allows you to store an array of data.

But, if you're using MySQL 5.6 or lower, this column type does not exist. And actually, that's not a problem! In that case, Doctrine is smart enough to use a normal text field, json_encode() your array when saving, and json_decode() it automatically when we query. So, no matter what database you use, you can use this json Doctrine column type.

But, here's the catch. Open config/packages/doctrine.yaml. One of the keys here is server_version, which is set to 5.7 by default:

31 lines | config/packages/doctrine.yaml
// ... lines 1 - 7
doctrine:
dbal:
// ... lines 10 - 11
server_version: '5.7'
// ... lines 13 - 31

This tells Doctrine that when it interacts with the database, it should expect that our database has all the features supported by MySQL 5.7, including that native JSON column type. If your computer, or more importantly, if your production database is using MySQL 5.6, then you'll get a huge error when Doctrine tries to make queries using the native MySQL JSON column type.

If you're in this situation, just set this back to 5.6:

31 lines | config/packages/doctrine.yaml
// ... lines 1 - 7
doctrine:
dbal:
// ... lines 10 - 11
server_version: '5.6'
// ... lines 13 - 31

Doctrine will then create a normal text column for the JSON field.

Generating the Migration

Ok, now run:

php bin/console make:migration

Perfect! Go check that file out in src/Migrations:

29 lines | src/Migrations/Version20180830012659.php
// ... lines 1 - 10
final class Version20180830012659 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// ... lines 15 - 17
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', first_name VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
}
// ... lines 20 - 27
}

And... nice! CREATE TABLE user. Look at the roles field: a LONGTEXT column. If you kept your server_version at 5.7, this would be a json column.

Let's run this:

php bin/console doctrine:migrations:migrate

Adding Fixtures

One last step: we need to add some dummy users into the database. Start with:

php bin/console make:fixtures

Call it UserFixture. Go check that out: src/DataFixtures/UserFixture.php:

18 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 2
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class UserFixture extends Fixture
{
public function load(ObjectManager $manager)
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}

If you watched our Doctrine tutorial, you might remember that we created a special BaseFixture with some sweet shortcut methods. Before I started recording this tutorial, based on some feedback from you nice people, I made a few improvements to that class. Go team!

92 lines | src/DataFixtures/BaseFixture.php
// ... lines 1 - 9
abstract class BaseFixture extends Fixture
{
// ... lines 12 - 21
public function load(ObjectManager $manager)
{
$this->manager = $manager;
$this->faker = Factory::create();
$this->loadData($manager);
}
// ... lines 29 - 90
}

The way you use this class is still the same: extend BaseFixture and update the load() method to be protected function loadData(). I'll remove the old use statement.

17 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 2
namespace App\DataFixtures;
use Doctrine\Common\Persistence\ObjectManager;
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
// ... lines 11 - 14
}
}

Inside, call $this->createMany(). The arguments to this method changed a bit since the last tutorial:

92 lines | src/DataFixtures/BaseFixture.php
// ... lines 1 - 9
abstract class BaseFixture extends Fixture
{
// ... lines 12 - 29
/**
* Create many objects at once:
*
* $this->createMany(10, function(int $i) {
* $user = new User();
* $user->setFirstName('Ryan');
*
* return $user;
* });
*
* @param int $count
* @param string $groupName Tag these created objects with this group name,
* and use this later with getRandomReference(s)
* to fetch only from this specific group.
* @param callable $factory
*/
protected function createMany(int $count, string $groupName, callable $factory)
{
for ($i = 0; $i < $count; $i++) {
$entity = $factory($i);
if (null === $entity) {
throw new \LogicException('Did you forget to return the entity object from your callback to BaseFixture::createMany()?');
}
$this->manager->persist($entity);
// store for usage later as groupName_#COUNT#
$this->addReference(sprintf('%s_%d', $groupName, $i), $entity);
}
}
// ... lines 61 - 90
}

Pass this 10 to create 10 users. Then, pass a "group name" - main_users. Right now, this key is meaningless. But later, we'll use it in a different fixture class to relate other objects to these users. Finally, pass a callback with an $i argument:

23 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 7
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
// ... lines 13 - 17
});
$manager->flush();
}
}

This will be called 10 times and our job inside is simple: create a User, put some data on it and return!

Do it! $user = new User():

23 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 4
use App\Entity\User;
// ... lines 6 - 7
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
$user = new User();
// ... lines 14 - 17
});
// ... lines 19 - 20
}
}

Then $user->setEmail() with sprintf() spacebar%d@example.com. For the %d wildcard, pass $i:

23 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 7
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
$user = new User();
$user->setEmail(sprintf('spacebar%d@example.com', $i));
// ... lines 15 - 17
});
// ... lines 19 - 20
}
}

Which will be one, two, three, four, five, six, seven, eight, nine, ten for the 10 calls.

The only other field is first name. To set this, we an use Faker, which we already setup inside BaseFixture: $this->faker->firstName:

23 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 7
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
$user = new User();
$user->setEmail(sprintf('spacebar%d@example.com', $i));
$user->setFirstName($this->faker->firstName);
// ... lines 16 - 17
});
// ... lines 19 - 20
}
}

Finally, at the bottom, return $user:

23 lines | src/DataFixtures/UserFixture.php
// ... lines 1 - 7
class UserFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
$user = new User();
$user->setEmail(sprintf('spacebar%d@example.com', $i));
$user->setFirstName($this->faker->firstName);
return $user;
});
// ... lines 19 - 20
}
}

And... we're done! This step had nothing to do with security: this is just boring Doctrine & PHP code inside a fancy createMany() method to make life easier.

Load 'em up:

php bin/console doctrine:fixtures:load

Let's see what these look like:

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

Nice! Our User class is done! Now, it's time to add a login form and a login form authenticator: the first way that we'll allow our users to login.