Customizing the User Entity
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:
// ... 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:
// ... 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:
// ... 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:
// ... 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
:
// ... 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
:
// ... 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!
// ... 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.
// ... 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:
// ... 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:
// ... 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()
:
// ... 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
:
// ... 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
:
// ... 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
:
// ... 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.
Fun fact, I'm using postgres, and it appears that my User entity created a table that conflicted with postgres users.
I found this when I got the following error from `AbstractPostgreSQLDriver`:
`SQLSTATE[42601]: Syntax error: 7 ERROR: syntax error at or near "user"
LINE 1: DELETE FROM user`
If anyone else has this problem, it's easy to change the table name for the entity using annotations. Check out https://stackoverflow.com/q....