Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Creating a User Entity

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We won't talk about security in this tutorial. But even still, we do need the concept of a user... because each treasure in the database will be owned by a user... or really, by a dragon. Later, we'll use this to allow API users to see which treasures belong to which user and a bunch more.

make:user

So, let's create that User class. Find your terminal and run:

php bin/console make:user

We could use make:entity, but make:user will set up a bit of the security stuff that we'll need in a future tutorial. Let's call the class User, yes we are going to store these in the database, and set email as the main identifier field.

Next it asks if we need to hash and check user passwords. If the hashed version of user passwords will be stored in your system, say yes to this. If your users won't have passwords - or some external system checks the passwords - answer no. I'll say yes to this.

This didn't do much... in a good way! It gave us a User entity, the repository class... and a small update to config/packages/security.yaml. Yup, it just sets up the user provider: nothing special. And again, we'll talk about that in a future tutorial.

Adding a username Property

Ok, inside the src/Entity/ directory, we have our new User entity class with id, email and password properties... and getters and setters below. Nothing fancy. This implements two interfaces that we need for security... but those aren't important right now.

... lines 1 - 2
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
... lines 30 - 99
}

Oh, but I do want to add one more field to this class: a username that we can show in the API.

So, spin back over to your terminal and this time run:

php bin/console make:entity

Update the User class, add a username property, 255 length is good, not null... and done. Hit enter one more time to exit.

Back over on the class... perfect! There's the new field. While we're here, add unique: true to make this unique in the database.

... lines 1 - 11
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 14 - 30
#[ORM\Column(length: 255, unique: true)]
private ?string $username = null;
... lines 33 - 114
}

Entity done! Let's make a migration for it. Back at the terminal run:

symfony console make:migration

Then... spin over and open that new migration file. No surprises: it creates the user table:

... 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 Version20230104193724 extends AbstractMigration
{
... lines 15 - 19
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON "user" (username)');
}
... lines 28 - 35
}

Close that up and run it with:

symfony console doctrine:migrations:migrate

Adding the Factory & Fixtures

Sweet! Though, I think our new entity deserves some juicy data fixtures. Let's use Foundry like we did for DragonTreasure. Start by running

php bin/console make:factory

to generate the factory for User.

Like before, in the src/Factory/ directory, we have a new class - UserFactory - which is really good at creating User objects. The main thing we need to tweak is getDefaults() to make the data even better. I'm going to paste in new contents for the entire class, which you can copy from the code block on this page.

... lines 1 - 2
namespace App\Factory;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;
/**
* @extends ModelFactory<User>
... lines 14 - 29
*/
final class UserFactory extends ModelFactory
{
const USERNAMES = [
'FlamingInferno',
'ScaleSorcerer',
'TheDragonWithBadBreath',
'BurnedOut',
'ForgotMyOwnName',
'ClumsyClaws',
'HoarderOfUselessTrinkets',
];
... lines 42 - 47
public function __construct(
private UserPasswordHasherInterface $passwordHasher
)
{
parent::__construct();
}
... lines 54 - 59
protected function getDefaults(): array
{
return [
'email' => self::faker()->email(),
'password' => 'password',
'username' => self::faker()->randomElement(self::USERNAMES) . self::faker()->randomNumber(3),
];
}
... lines 68 - 71
protected function initialize(): self
{
return $this
->afterInstantiate(function(User $user): void {
$user->setPassword($this->passwordHasher->hashPassword(
$user,
$user->getPassword()
));
})
;
}
protected static function getClass(): string
{
return User::class;
}
}

This updates getDefaults() to have a little more pizazz and sets the password to password. I know, creative. I'm also leveraging an afterInstantiation hook to hash that password.

Finally, to actually create some fixtures, open up AppFixtures. Pretty simple here: UserFactory::createMany() and let's create 10.

... lines 1 - 5
use App\Factory\UserFactory;
... lines 7 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
DragonTreasureFactory::createMany(40);
UserFactory::createMany(10);
}
}

Let's see if that worked! Spin over and run:

symfony console doctrine:fixtures:load

No errors!

Status check: we have a User entity and we created a migration for it. Heck, we even loaded some schweet data fixtures! But it is not, yet, part of our API. If you refresh the documentation, there's still only Treasure.

Let's make this part of our API next.

Leave a comment!

3
Login or Register to join the conversation
jmsche Avatar
jmsche Avatar jmsche | posted 6 months ago | edited

Hi,

I'm going to paste in new contents for the entire class, which you can copy from the code block on this page.

Unfortunately there's no code block :/

[Edit] You can use the following (taken from finished project code):

declare(strict_types=1);

namespace App\Factory;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;

/**
 * @extends ModelFactory<User>
 *
 * @method        User|Proxy create(array|callable $attributes = [])
 * @method static User|Proxy createOne(array $attributes = [])
 * @method static User|Proxy find(object|array|mixed $criteria)
 * @method static User|Proxy findOrCreate(array $attributes)
 * @method static User|Proxy first(string $sortedField = 'id')
 * @method static User|Proxy last(string $sortedField = 'id')
 * @method static User|Proxy random(array $attributes = [])
 * @method static User|Proxy randomOrCreate(array $attributes = [])
 * @method static UserRepository|RepositoryProxy repository()
 * @method static User[]|Proxy[] all()
 * @method static User[]|Proxy[] createMany(int $number, array|callable $attributes = [])
 * @method static User[]|Proxy[] createSequence(array|callable $sequence)
 * @method static User[]|Proxy[] findBy(array $attributes)
 * @method static User[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
 * @method static User[]|Proxy[] randomSet(int $number, array $attributes = [])
 */
final class UserFactory extends ModelFactory
{
    const USERNAMES = [
        'FlamingInferno',
        'ScaleSorcerer',
        'TheDragonWithBadBreath',
        'BurnedOut',
        'ForgotMyOwnName',
        'ClumsyClaws',
        'HoarderOfUselessTrinkets',
    ];

    public function __construct(
        private readonly UserPasswordHasherInterface $passwordHasher,
    ) {
        parent::__construct();
    }

    protected function getDefaults(): array
    {
        return [
            'email' => self::faker()->email(),
            'password' => 'password',
            'username' => self::faker()->randomElement(self::USERNAMES) . self::faker()->randomNumber(3),
        ];
    }

    protected function initialize(): self
    {
        return $this
            ->afterInstantiate(function(User $user): void {
                $user->setPassword($this->passwordHasher->hashPassword(
                    $user,
                    $user->getPassword()
                ));
            })
        ;
    }

    protected static function getClass(): string
    {
        return User::class;
    }
}
Reply

Hey Jmsche,

Code blocks are available for this chapter now! Thank you for your patience :)

Cheers!

Reply

Hey Jmsche,

We're sorry for the delay, we're working on adding more code blocks to the recently published chapters and will add them shortly. For now, thanks for sharing it in the comments with others :)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.0.8
        "doctrine/annotations": "^1.0", // 1.14.2
        "doctrine/doctrine-bundle": "^2.8", // 2.8.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.0
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.64.1
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.15.3
        "symfony/asset": "6.2.*", // v6.2.0
        "symfony/console": "6.2.*", // v6.2.3
        "symfony/dotenv": "6.2.*", // v6.2.0
        "symfony/expression-language": "6.2.*", // v6.2.2
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.3
        "symfony/property-access": "6.2.*", // v6.2.3
        "symfony/property-info": "6.2.*", // v6.2.3
        "symfony/runtime": "6.2.*", // v6.2.0
        "symfony/security-bundle": "6.2.*", // v6.2.3
        "symfony/serializer": "6.2.*", // v6.2.3
        "symfony/twig-bundle": "6.2.*", // v6.2.3
        "symfony/ux-react": "^2.6", // v2.6.1
        "symfony/validator": "6.2.*", // v6.2.3
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.0
        "symfony/yaml": "6.2.*" // v6.2.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.2.*", // v6.2.1
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/stopwatch": "6.2.*", // v6.2.0
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.4
        "zenstruck/foundry": "^1.26" // v1.26.0
    }
}
userVoice