If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe're not going to talk specifically about security in this tutorial - we'll do that in our next course and give it proper attention. But, even forgetting about security and logging in and all that, there's a pretty good chance that your API will have some concept of "users". In our case, a "user" will post a cheese listing and becomes its "owner". And maybe later, in order to buy a CheeseListing, one User might send a message to another User. It's time to take our app to the next level by creating that entity.
And even though I'm telling you not to think about security, instead of creating the user entity with make:entity
like I normally would, I'm actually going to use make:user
,
php bin/console make:user
Yea, this will set up a few security-related things... but nothing that we'll use yet. Watch part 2 of this series for all that stuff.
Anyways, call the class User
, and I do want to store users in the database. For the unique display name, I'm going to have users log in via email, so use that. And then:
Does this app need to hash or check user passwords?
We'll talk more about this in the security tutorial. But if users will need to log in to your site via a password and your app will be responsible for checking to see if that password is valid - you're not just sending the password to some other service to be verified - then answer yes. It doesn't matter if the user will enter the password via an iPhone app that talks to your API or via a login form - answer yes if your app is responsible for managing user passwords.
I'll use the Argon2i password hasher. But! If you don't see this question, that's ok! Starting in Symfony 4.3, you don't need to choose a password hashing algorithm because Symfony can choose the best available automatically. Really cool stuff.
Let's go see what this did! I'm happy to say... not much! First, we now have a User
entity. And... there's nothing special about it: it does have a few extra security-related methods, like getRoles()
, getPassword()
, getSalt()
and eraseCredentials()
, but they won't affect what we're doing. Mostly we have a normal, boring entity with $id
, $email
, a $roles
array property, and $password
, which will eventually store the hashed password.
... lines 1 - 2 | |
namespace App\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
/** | |
* @ORM\Entity(repositoryClass="App\Repository\UserRepository") | |
*/ | |
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 = []; | |
/** | |
* @var string The hashed password | |
* @ORM\Column(type="string") | |
*/ | |
private $password; | |
... lines 35 - 112 | |
} |
This also created the normal UserRepository
and made a couple of changes to security.yaml
: it set up encoders
- this might say auto
for you, thanks to the new Symfony 4.3 feature - and the user provider. All things to talk more about later. So... just forget they're here and instead say... yay! We have a User
entity!
security: | |
encoders: | |
App\Entity\User: | |
algorithm: argon2i | |
... lines 5 - 6 | |
providers: | |
# used to reload user from session & other features (e.g. switch_user) | |
app_user_provider: | |
entity: | |
class: App\Entity\User | |
property: email | |
... lines 13 - 33 |
... lines 1 - 2 | |
namespace App\Repository; | |
use App\Entity\User; | |
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; | |
use Symfony\Bridge\Doctrine\RegistryInterface; | |
/** | |
* @method User|null find($id, $lockMode = null, $lockVersion = null) | |
* @method User|null findOneBy(array $criteria, array $orderBy = null) | |
* @method User[] findAll() | |
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) | |
*/ | |
class UserRepository extends ServiceEntityRepository | |
{ | |
public function __construct(RegistryInterface $registry) | |
{ | |
parent::__construct($registry, User::class); | |
} | |
... lines 21 - 49 | |
} |
Thanks to the command, the entity has an email
property, and I'm planning to make users log in by using that. But I also want each user to have a "username" that we can display publicly. Let's add that: find your terminal and run:
php bin/console make:entity
Update User
and add username
as a string
, 255, not nullable in the database, and hit enter to finish.
Now open up User
... and scroll down to getUsername()
. The make:user
command generated this and returned $this->email
... because that's what I chose as my "display" name for security. Now that we really do have a username field, return $this->username
.
... lines 1 - 10 | |
class User implements UserInterface | |
{ | |
... lines 13 - 35 | |
/** | |
* @ORM\Column(type="string", length=255) | |
*/ | |
private $username; | |
... lines 40 - 62 | |
public function getUsername(): string | |
{ | |
return (string) $this->username; | |
} | |
... lines 67 - 124 | |
} |
Oh, and while we're making this class, just, amazing, the make:user
command knew that email
should be unique, so it added unique=true
. Let's also add that to username
: unique=true
.
... lines 1 - 35 | |
/** | |
* @ORM\Column(type="string", length=255, unique=true) | |
*/ | |
private $username; | |
... lines 40 - 126 |
That is a nice entity! Let's sync up our database by running:
php bin/console make:migration
Move over... and double-check the SQL: CREATE TABLE user
- looks good!
... lines 1 - 12 | |
final class Version20190509185722 extends AbstractMigration | |
{ | |
... lines 15 - 19 | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), UNIQUE INDEX UNIQ_8D93D649F85E0677 (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); | |
} | |
... lines 27 - 34 | |
} |
Run it with:
php bin/console doctrine:migration:migrate
Perfect! We have a gorgeous new Doctrine entity... but as far as API Platform is concerned, we still only have one API resource: CheeseListing
.
Next: let's expose User
as an API Resource and use all of our new knowledge to perfect that new resource in... about 5 minutes.
Using the latest Symfony binary and following along the tutorial for API Platform (which is still based on Symfony 4/5), I get the following error:
>bin/console make:entitiy
In SecurityExtension.php line 307:
Invalid firewall "main": user provider "users_in_memory" not found.
It seems that the command bin/console make:user
replaces
providers:
users_in_memory: { memory: null }
with
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
but leaves
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
provider: users_in_memory
intact. We no longer have a users_in_memory
provider, which leads to the error.
The make:user
command should probably result in a coherent state of the security.yaml
file, so either users_in_memory
or app_user_provider
, not a mix of both.
TL:DR:
Replace provider: users_in_memory
with provider: app_user_provider
in security.yaml
under security:firewalls:main
I hope this does not cause problems down the road, but so far everything worked as expected.
Hi Jonas!
Thanks for posting this! It seems like a bug in MakerBundle - it's possible it's still a bug, or it may have been fixed (I work on MakerBundle, but we do so much that I can't remember when we do things 🙃). Anyways - thanks for posting - and if you ARE using the latest version of MakerBundle, it would be awesome if you could open an issue about this on https://github.com/symfony/maker-bundle
Cheers!
Hi Ryan, thanks for the quick answer!
My maker bundle version is not current, as I followed the advice in chapter 2 and installed version 1.11.
So maybe it is fixed, just not if you're following along with the course. :)
All the best,
Jonas
If you're using Symfony 5.3, you'll get the following error when trying to add new fields to the User entity:
PHP Fatal error: Class App\Entity\User contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Symfony\Component\Security\Core\User\UserInterface::getUsername) in /mnt/c/laragon/www/api-platform/src/Entity/User.php on line 13
This is because the method getUsername was deprecated in favor of getUserIdentifier, but the UserInterface still implements the old method, not the new one.
You can find more information here: <a href="https://github.com/symfony/symfony/pull/41493">https://github.com/symfony/symfony/pull/41493</a>.
Just add the following code and then try again:
`
/**
* @deprecated in Symfony 5.3
*/
public function getUsername(): string
{
return $this->getUserIdentifier();
}
`
Hey Andre,
Thank you for sharing the solution with others! Looks like you also need to implement that 1 abstract method as well as I can see from the error message. I think we will add a note about it, thanks!
Cheers!
Yep, this is my bad. Well, once 5.3.1 came out, we needed to merge and tag this PR- https://github.com/symfony/...
That's my job - I've been really busy, but it will happen soon!
Cheers!
Hello!
I'm using MongoDB instead of MySQL and I figured I couldn't use the make command to create a Document.
But I wanted to use the Symfony built-in authentication mechanism with json_login.
Is it possible for it to work by simply replacing Entity with Document in the app_user_provider?
Thanks!
Hey @Med!
Yep! Symfony's security system doesn't care if your User object is loaded via Doctrine ORM, Doctrine ODM or via an alien spaceship ;). So that's good. However, I don't believe that the ODM integrates *that* well - you can't just change the config. But, it's still pretty easy:
1) Create a class called UserProvider and make it implement UserProviderInterface
2) Fill in the methods - loadUserByUsername() is the key method that json_login will call
3) Pass your service id (well, class name) to the "providers" config in security.yaml
My answer is a big vague, because the docs are here! https://symfony.com/doc/cur...
Let me know if that helps!
Cheers!
10.1.3 mariaDB
The JSON alias was added in maria DB 10.2.7 so I have to upgrade mariaDB? is that correct
in this case you have 2 options.
First is to upgrade your server
Second is more interesting you can try configure doctrine.dbal.server_version
to correspond you server version
Example:
doctrine:
dbal:
server_version: 'mariadb-10.1.3'
Hope it will help you, Cheers!
I choose the second option and I changed the server version but I got the following error: SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: NO)
I added a password in .env file DATABASE_URL=mysql://root:0000@127.0.0.1:3306/Glabre.
An exception occurred in driver: SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: YES)
Hey hanen
Oh I'm so sorry for late message, I missed your comment at all =(
I hope you you solved this issue, because honestly I have no Idea why this error occurred, Probably it was some user password misconfiguration
Cheers!
upgrading mariadb from 10.1 to 10.2.x & of course setup a new MySQL root user password by default run as administrator :))))
;))
Serveur : 127.0.0.1 via TCP/IP
Type de serveur : MariaDB
Connexion au serveur : SSL n'est pas utilisé Documentation
Version du serveur : 10.2.9-MariaDB - mariadb.org binary distribution
Version du protocole : 10
Utilisateur : root@localhost
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.17.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}
If you're running mariadb and getting the error "SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes" when running the migration, edit User.php and limit the username length to 191:
` /**