Inek

Sign up y Login en PHP con Clean Architecture IV

¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.

Cerrar

01 Jul 2024

Cuarta iteración con auth-lib, una librería que estoy desarrollando para usar en mis proyectos, y para quien le sea útil, siguiendo los lineamientos de Clean Architecture. Ya tengo una versión funcional del caso de uso SignUp, con posibilidad de ser extendido para cubrir diferentes escenarios. Ahora es el turno de empezar con el caso de uso Login, que es el tema de este post.

Este es el link al código a partir del cual voy a trabajar en este post y este es el link al código resultante.

Requerimiento

En su versión inicial, el Login deberá permitirnos comprobar la existencia de un usuario y, si existe, comprobar que el password es válido. El resultado de este proceso debe ser informado a través del output port de nuestro caso de uso. Vamos con la primer iteración:

class LoginInteractor implements LoginInputPort
{
...
    public function login(LoginRequest $request): void
    {
        $user = $this->userRepository->findByUsername($request->username);
        $this->output->userLoggedIn(new LoginResponse($user->getId()));
    }
...
}

Esta es la versión más elemental del login. Ahora añadiremos las dos verificaciones que mencionamos más arriba: que el usuario exista y la contraseña sea válida:

class LoginInteractor implements LoginInputPort
{
...
    public function login(LoginRequest $request): void
    {
        $user = $this->userRepository->findByUsername($request->username);
        
        if (is_null($user)) {
            $this->output->userNotFound();
            return;
        }

        if (!$user->passwordMatches($request->password)) {
            $this->output->passwordNotMatch();
            return;
        }

        $this->output->userLoggedIn(new LoginResponse($user->getId()));
    }
...

Implementar la validación de contraseña

En su versión inicial, la clase User no tiene mucha lógica de negocio. Ahora vamos a necesitar que sea capaz de validar una contraseña. Para seguir con las buenas prácticas, UserRepository no debería almacenar una contraseña en texto plano, sino un hash de ésta. De este modo, la clase User tampoco necesita tener almacenado el password tal como lo ingresa el usuario:

class User
{
    private UserId $id;

    public function __construct(
        public readonly string $username,
        private string $passwordHash = ''
    ) {
    }

    public function passwordMatches(string $password): bool
    {
        return password_verify($password, $this->passwordHash);
    }

    public function hashPassword(string $password): self
    {
        $this->passwordHash = password_hash($password, PASSWORD_DEFAULT);
        return $this;
    }
...

Ahora nuestra clase User será capaz de validar una contraseña de manera segura utilizando passwordMatches.

El cambio que hemos realizado a nuestro usuario afecta a nuestro SignUp, más precisamente a la factory encargada de crear los usuarios por lo que vamos a ajustarla:

     public function createUserFromRequest(
         SignUpRequest $request
     ): User {
//        return new User(
//            $request->username,
//            $request->password
//        );
        return (new User($request->username))
            ->hashPassword($request->password);
     }
 }

Con este cambio, la factory que definimos en el caso de uso SignUp se encarga de configurar la contraseña de nuestro usuario.

Por último, para completar este proceso de robustecimiento de la clase User, deberemos añadir un método para obtener el hash para que nuestro UserRepository pueda almacenar ese valor, que eventualmente será utilizado para instanciar un usuario cuando sea recuperado de la base de datos:

class User
{
...
    public function getPasswordHash(): string
    {
        return $this->passwordHash;
    }
...
}

Ejemplo de uso de Login

Para ejemplificar los distintos escenarios del Login, creamos un usuario en el repositorio de usuarios, creamos un presenter e instanciamos el interactor:

$user = (new User('some@example.com'))
        ->hashPassword('some-password')
        ->setId(new UserId('some-user-id'));

$userRepository = new InMemoryUserRepository();
$userRepository->createUser($user);

$presenter = new class implements LoginOutputPort {
    public function userLoggedIn(LoginResponse $response): void
    {
        echo "User logged in with ID: {$response->userId}\n";
    }

    public function userNotFound(): void
    {
        echo "User not found\n";
    }

    public function passwordNotMatch(): void
    {
        echo "Password does not match\n";
    }
};

$loginInteractor = new LoginInteractor($userRepository, $presenter);

$request = new LoginRequest('some@not-found.com', 'some-password');
$loginInteractor->login($request);
// prints "User not found"

$request = new LoginRequest('some@example.com', 'some-wrong-password');
$loginInteractor->login($request);
// prints "Password does not match"

$request = new LoginRequest('some@example.com', 'some-password');
$loginInteractor->login($request);
// prints "User logged in with ID: <some id>"

Conclusión y próximos pasos

Tenemos una librería capaz de registrar usuarios e iniciar sesión. Hay un dato más a considerar para los usuarios, además del usuario y contraseña, que también aparecerá en la gran mayoría de los proyectos y por eso es necesario contemplarlo. En la siguiente iteración vamos a permitir que el objeto user tenga un rol.

¿Qué te pareció el post?

No hay comentarios.