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 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.
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.
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
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.
Hey Jmsche,
Code blocks are available for this chapter now! Thank you for your patience :)
Cheers!
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!
// 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
}
}
Hi,
Unfortunately there's no code block :/
[Edit] You can use the following (taken from finished project code):