Customizing the User Class

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

What's cool about the User class is that... it's our class! As long as we implement UserInterface, we can add whatever else we want:

... lines 1 - 7
use Symfony\Component\Security\Core\User\UserInterface;
... lines 9 - 12
class User implements UserInterface
{
... lines 15 - 113
}

For example, I'd like to store the first name of my users. So let's go add a property for that. At your terminal, run:

symfony console make:entity

We'll edit the User entity, add a firstName property, have it be a string, 255 length... and say "yes" to nullable. Let's make this property optional in the database.

Done! Back over in the User class, no surprises! We have a new property... and new getter and setter methods:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 31
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
... lines 36 - 119
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
}

Go generate a migration for our new User. At the terminal, run

symfony console make:migration

Cool! Spin over and open that up to make sure it's not hiding any surprises:

... lines 1 - 2
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20211001172813 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, first_name VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE user');
}
}

Awesome: CREATE TABLE user with id, email, roles and first_name columns. Close this... and run it:

symfony console doctrine:migrations:migrate

Success!

Adding User Fixtures

And because the User entity is... just a normal Doctrine entity, we can also add dummy users to our database using the fixtures system.

Open up src/DataFixtures/AppFixtures.php. We're using Foundry to help us load data. So let's create a new Foundry factory for User. Since we're having SO much fun running commands in this video, let's sneak in one... or three more:

symfony console make:factory

Yup! We want one for User. Go open it up: src/Factory/UserFactory.php:

... lines 1 - 2
namespace App\Factory;
use App\Entity\User;
use App\Repository\UserRepository;
use Zenstruck\Foundry\RepositoryProxy;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
... lines 10 - 28
final class UserFactory extends ModelFactory
{
public function __construct()
{
parent::__construct();
// TODO inject services if required (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services)
}
protected function getDefaults(): array
{
return [
// TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
'email' => self::faker()->text(),
'roles' => [],
'firstName' => self::faker()->text(),
];
}
protected function initialize(): self
{
// see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
return $this
// ->afterInstantiate(function(User $user) {})
;
}
protected static function getClass(): string
{
return User::class;
}
}

Our job in getDefaults() is to make sure that all of the required properties have good default values. Set email to self::faker()->email(), I won't set any roles right now and set firstName to self::faker()->firstName():

... lines 1 - 28
final class UserFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
'email' => self::faker()->email(),
'firstName' => self::faker()->firstName(),
];
}
... lines 45 - 57
}

Cool! Over in AppFixtures, at the bottom, create a user: UserFactory::createOne(). But use a specific email so we can log in using this later. How about, abraca_admin@example.com:

... lines 1 - 11
use App\Factory\UserFactory;
... lines 13 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 20 - 41
AnswerFactory::new(function() use ($questions) {
... lines 43 - 45
})->needsApproval()->many(20)->create();
UserFactory::createOne(['email' => 'abraca_admin@example.com']);
... lines 49 - 51
}
}

Then, to fill out the system a bit, add UserFactory::createMany(10):

... lines 1 - 11
use App\Factory\UserFactory;
... lines 13 - 15
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 20 - 47
UserFactory::createOne(['email' => 'abraca_admin@example.com']);
UserFactory::createMany(10);
... lines 50 - 51
}
}

Let's try it! Back at the terminal, run:

symfony console doctrine:fixtures:load

No errors! Check out the new table:

symfony console doctrine:query:sql 'SELECT * FROM user'

And... there they are! Now that we have users in the database, we need to add one or more ways for them to authenticate. It's time to build a login form!

Leave a comment!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}