Inek

Sign up y Login en PHP con Clean Architecture

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

Cerrar

20 Jun 2024

¿Te has encontrado creando una y otra vez los casos de uso para registrar e iniciar sesión en todos tus proyectos, ya sean personales o profesionales? En este post, voy a compartir mis notas personales sobre cómo desarrollar una librería en PHP utilizando Clean Architecture para gestionar la creación de usuarios y el inicio de sesión. Esta es una guía que estoy creando principalmente para mí mismo, para afianzar conceptos y de paso, para reutilizar en futuros proyectos donde los grandes proveedores de gestión de usuarios, como Auth0 o Supabase, aún no son una opción viable. Espero que estas notas también te sean útiles y te ahorren tiempo y esfuerzo en tus propios proyectos.

La primer versión del registro de usuarios consiste en permitir la creación de cuentas con email y contraseña. El caso de uso deberá asegurar que el email es válido y no fue utilizado previamente.

El código de este post se encuentra en el repositorio auth-lib.

Implementando el Sign up

Siguiendo los lineamientos de Clean Architecture voy a definir las interfaces para el interactor o caso de uso:

// Input interface que implementará el interactor
interface SignUpInputPort {
    public function signUp(SignUpRequest $request): void;
}

// Output interface que deberá implementar la capa de presentación
interface SignUpOutputPort {
    public function userSignedUp(SignUpResponse $response): void;
    public function userAlreadyExists(string $message): void;
    public function invalidUsername(string $message): void;
}

Estos serán los modelos de request, con los datos que espera recibir el interactor, y response, que es donde el interactor reflejará el resultado de la operación:

class SignUpRequest {
    public string $username;
    public string $password;

    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
}

class SignUpResponse
{
    public function __construct(public readonly UserId $userId)
    {
    }
}

De momento, para dar de alta un usuario solo vamos a requerir usuario y contraseña. La respuesta esperada será un identificador de usuario:

class UserId
{
    public function __construct(private string $value)
    {
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

Finalmente, esta es la implementación de la lógica del caso de uso:

class SignUpInteractor implements SignUpInputPort
{
    public function __construct(
        private UserRepository $userRepository,
        private SignUpOutputPort $output
    ) {
    }

    public function signUp(SignUpRequest $request): void
    {
        if (!filter_var($request->username, FILTER_VALIDATE_EMAIL)) {
            $this->output->invalidUsername($request->username);
            return;
        }

        if ($this->userRepository->existsByUsername($request->username)) {
            $this->output->userAlreadyExists($request->username);
            return;
        }

        $userId = $this->userRepository->createUser(
            $request->username,
            $request->password
        );

        $this->output->userSignedUp(new SignUpResponse($userId));
    }
}

De acuerdo a cada escenario, el interactor invocará un método del output port.

Ejemplo

Veamos un ejemplo de cómo usar lo que hemos implementado hasta aquí:

<?php
use Imefisto\AuthLib\Infrastructure\Persistence\InMemoryUserRepository;
use Imefisto\AuthLib\UseCases\SignUp\SignUpInteractor;
use Imefisto\AuthLib\UseCases\SignUp\SignUpOutputPort;
use Imefisto\AuthLib\UseCases\SignUp\SignUpRequest;
use Imefisto\AuthLib\UseCases\SignUp\SignUpResponse;

require_once __DIR__ . '/../vendor/autoload.php';

$userRepository = new InMemoryUserRepository();

// Create the presenter
$presenter = new class implements SignUpOutputPort {
    public function userSignedUp(SignUpResponse $response): void
    {
        echo "User signed up with ID: {$response->userId}\n";
    }

    public function userAlreadyExists(string $username): void
    {
        echo "User already exists: $username\n";
    }

    public function invalidUsername(string $username): void
    {
        echo "Invalid username: $username\n";
    }
};

$signUpUseCase = new SignUpInteractor($userRepository, $presenter);
$request = new SignUpRequest('user@example.com', 'securepassword');

// expects User signed up with ID: user@example.com
$signUpUseCase->signUp($request);

// expects User already exists: user@example.com
$signUpUseCase->signUp($request);

$invalidUsernameRequest = new SignUpRequest('userexample', 'securepassword');
$signUpUseCase->signUp($invalidUsernameRequest);
// expects Invalid username: userexample

Básicamente hemos creado una implementación de SignUpOutputPort y con ella, sumado a una implementación del UserRepository hemos instanciado el interactor.

Conclusión y próximos pasos

Hemos implementado una versión básica de un registro de usuarios basado en username (email) y contraseña. El código admite muchos puntos de mejora que iré añadiendo en próximos posts. Algunos de esos puntos:

Referencias

The Clean Code Blog by Robert C. Martin (Uncle Bob)

¿Qué te pareció el post?

No hay comentarios.