Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Crear una entidad de usuario

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

No hablaremos de seguridad en este tutorial. Pero aun así, necesitamos el concepto de usuario... porque cada tesoro de la base de datos será propiedad de un usuario... o, en realidad, de un dragón. Más adelante, utilizaremos esto para permitir a los usuarios de la API ver qué tesoros pertenecen a qué usuario y un montón de cosas más.

make:user

Así pues, vamos a crear esa clase User. Busca tu terminal y ejecuta:

php bin/console make:user

Podríamos utilizar make:entity, pero make:user configurará un poco las cosas de seguridad que necesitaremos en un tutorial futuro. Llamemos a la clase User, sí, vamos a almacenarlos en la base de datos, y establezcamos email como campo identificador principal.

A continuación nos pregunta si necesitamos hash y comprobar las contraseñas de los usuarios. Si en tu sistema se va a almacenar la versión hash de las contraseñas de los usuarios, di que sí. Si tus usuarios no van a tener contraseñas -o algún sistema externo comprueba las contraseñas- responde que no. Di que sí a esto.

Esto no hizo mucho... ¡en el buen sentido! Nos dio una entidad User, la clase repositorio... y una pequeña actualización de config/packages/security.yaml. Sí, sólo configura el proveedor de usuarios: nada especial. Y, de nuevo, hablaremos de ello en un futuro tutorial.

Añadir una propiedad de nombre de usuario

Vale, dentro del directorio src/Entity/, tenemos nuestra nueva clase de entidad User con las propiedadesid, email y password... y los getters y setters a continuación. Nada del otro mundo. Esto implementa dos interfaces que necesitamos para la seguridad... pero no son importantes ahora.

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

Ah, pero quiero añadir un campo más a esta clase: un username que podamos mostrar en la API.

Así que, vuelve a tu terminal y esta vez ejecuta:

php bin/console make:entity

Actualiza la clase User, añade una propiedad username, la longitud de 255 es buena, no nula... y listo. Pulsa enter una vez más para salir.

Vuelve a la clase... ¡perfecto! Ahí está el nuevo campo. Ya que estamos aquí, añadeunique: true para que sea único en la base de datos.

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

¡Entidad terminada! Hagamos una migración para ella. De vuelta en el terminal ejecuta:

symfony console make:migration

Y abre el nuevo archivo de migración. Sin sorpresas: crea la tablauser:

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

Ciérralo y ejecútalo con:

symfony console doctrine:migrations:migrate

Añadir la fábrica y los accesorios

¡Estupendo! Aunque, creo que nuestra nueva entidad se merece unas jugosas fijaciones de datos. Utilicemos Foundry como hicimos para DragonTreasure. Empieza ejecutando

php bin/console make:factory

para generar la fábrica de User.

Como antes, en el directorio src/Factory/, tenemos una nueva clase - UserFactory - que es realmente buena para crear objetos User. Lo principal que tenemos que retocar es getDefaults() para que los datos sean aún mejores. Voy a pegar nuevos contenidos para toda la clase, que puedes copiar del bloque de código de esta página.

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

Esto actualiza getDefaults() para que tenga un poco más de chispa y establece passworden password. Lo sé, creativo. También aprovecho un gancho de afterInstantiation para hacer hash de esa contraseña.

Por último, para crear realmente algunos accesorios, abre AppFixtures. Aquí es bastante sencillo: UserFactory::createMany() y vamos a crear 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);
}
}

Veamos si ha funcionado Gira y ejecuta:

symfony console doctrine:fixtures:load

¡Sin errores!

Comprobación de estado: tenemos una entidad User y hemos creado una migración para ella. Diablos, ¡incluso hemos cargado algunos schweet data fixtures! Pero todavía no forma parte de nuestra API. Si actualizas la documentación, todavía sólo aparece Treasure.

Hagamos que forme parte de nuestra API a continuación.

Leave a comment!

3
Login or Register to join the conversation
jmsche Avatar
jmsche Avatar jmsche | posted hace 6 meses | 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