make:user

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.

No matter how your users authenticate - a login form, social authentication, or an API key - your security system needs some concept of a user: some class that describes the "thing" that is logged in.

Yup, step 1 of authentication is to create a User class. And there's a command that can help us! Find your terminal and run:

symfony console make:user

As a reminder, symfony console is just a shortcut for bin/console... but because I'm using the Docker integration with the Symfony web server, calling symfony console allows the symfony binary to inject some environment variables that point to the Docker database. It won't matter for this command, but it will matter for any command that talks to the database.

Ok, question one:

The name of the user class

Typically, this will be User... though it would be cooler to use something like HumanoidEntity. If the "thing" that logs into your site would be better called a Company or University or Machine, use that name here.

Do you want to store user data in the database via Doctrine?

For us: that's a definite yes... but it's not a requirement. Your user data might be stored on some other server... though even in that case, it's often convenient to store some extra data in your local database... in which case you would also say yes here.

Next:

Enter a property name that will be the unique display name for the user.

I'm going to use email. This is not that important, and I'll explain how it's used in a few minutes. Finally:

Will this app need to hash and check user passwords?

You only need to say yes if it will be your application's responsibility to check the user's password when they log in. We are going to do this... but I'm going to say "no". We'll add it manually a bit later.

Hit enter and... done!

The User Class & Entity

Okay. What did this do? First, it created a User entity and a UserRepository... the exact same stuff you normally get from running make:entity. Let's go check out that new User class: src/Entity/User.php:

... 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)
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
... lines 31 - 113
}

First and foremost, this is a normal boring Doctrine entity: it has annotations - or maybe PHP 8 attributes for you - and an id. It is... just an entity: there is nothing special about it.

UserInterface & Deprecated Methods

The only thing that Symfony cares about is that your user class implements UserInterface. Hold Command or Ctrl and click to jump way into the core code to see this.

This interface really has just 3 methods: getUserIdentifier(), which you see documented above the interface, getRoles()... and another one way down here called eraseCredentials(). If you're confused about why I'm skipping all of these other methods, it's because they're deprecated. In Symfony 6, this interface will only have those 3: getUserIdentifier(), getRoles() and eraseCredentials().

In our User, class, if you scroll down, the make:user command implemented all of this for us. Thanks to how we answered one of its questions, getUserIdentier() returns the email:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 48
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
... lines 58 - 113
}

This... isn't too important: it's mostly just a visual representation of your User object... it's used in the web debug toolbar... and in a few optional systems, like the "remember me" system.

If you're using Symfony 5 like I am, you'll notice that the deprecated methods are still generated. They're needed just for backwards compatibility, and you can delete them once you're on Symfony 6.

The getRoles() method deals with permissions:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 66
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 78 - 113
}

more on that later. And then getPassword() and getSalt() are both deprecated:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 85
/**
* This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
*
* @see PasswordAuthenticatedUserInterface
*/
public function getPassword(): ?string
{
return null;
}
/**
* This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
... lines 105 - 113
}

You will still need a getPassword() method if you check passwords on your site - but we'll learn about that later. Finally, eraseCredentials() is part of UserInterface:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 105
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}

but it's not very important and we'll also talk about it later.

So at a high level... if you ignore the deprecated methods... and the not-so-important eraseCredentials(), the only thing that our User class needs to have is an identifier and a method that returns the array of roles that this user should have. Yup... it's mostly just a User entity.

"providers": The User Provider

The make:user command also made one tweak to our security.yaml file: you can see it right here:

security:
... lines 2 - 7
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 14 - 33

It added what's called a "user provider", which is an object that knows how to load your user objects... whether you're loading that data from an API or from a database. Because we're using Doctrine, we get to use the built-in entity provider: it knows how to fetch our users from the database using the email property.

I wanted you to see this change... but the user provider isn't important yet. I'll show you exactly how and where it's used as we go along.

Next: we have total control over how our User class looks. The power! So let's add a custom field to it and then load up our database with a nice set of dummy users.

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
    }
}