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.
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.
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.
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:
The Clean Code Blog by Robert C. Martin (Uncle Bob)
¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.
Cerrar