26 Jun 2024
Ya tenemos el caso de uso para Sign Up que desarrollamos en el primer post de esta serie y luego modificamos en el post anterior para permitir personalizar las validaciones. De momento, nuestro caso de uso solo permite registrar usuario y contraseña. ¿Cómo puede nuestra librería permitir gestionar más información durante el registro de usuarios? ¿Qué tal si queremos almacenar nombres, apellidos, o algún “organizationId” por nombrar algo? En este post vamos a encapsular los datos de usuario en un objeto que podrá ser extendido.
Este es el link al código que es el punto de partida del post y éste es el enlace al código resultante.
En nuestro Sign Up actual, una vez que se completan las validaciones, registramos el usuario con este código:
class SignUpInteractor implements SignUpInputPort
{
...
public function signUp(SignUpRequest $request): void
{
...
$userId = $this->userRepository->createUser(
$request->username,
$request->password
);
...Seguramente habrá muchos casos donde queramos almacenar más que username y password. ¿Cómo podemos ampliar los escenarios que podemos cubrir con esta librería? La respuesta está en encapsular los datos de usuario en una clase que luego podamos extender si hiciera falta:
class User
{
public function __construct(
public readonly string $username,
public readonly string $password
) {
}
}De momento, la clase User no encierra mayor misterio. Es el username y el password encapsulados en una clase. Para poder usar esta clase, debemos reemplazar la interface UserRepository para que reciba un objeto de tipo User en lugar de username y password:
interface UserRepository
{
..
// public function createUser(string $username, string $password): UserId;
public function createUser(User $user): UserId;
..Al modificar la interfaz, también debemos modificar aquellos lugares donde se usa la interfaz:
UserRepository src/Infrastructure/Persistence/InMemoryUserRepository.phpEstas son las consecuencias de modificar una interfaz. Cambiar el contrato que representa la interfaz afecta a todas las clases que consumen las implementaciones de dicha interfaz. Claramente estamos violando el principio open-closed, de los principios SOLID, que dice que las entidades en nuestro código deben estar abiertas para extensión pero cerradas para modificación.
Si quisiéramos apegarnos al principio open-closed podríamos añadir un nuevo método para guardar nuestro flamante nuevo User (ej: saveNewUser o saveUser o simplemente save), manteniendo el método anterior por compatibilidad. Como aquí estamos en una fase inicial del proyecto, podemos permitirnos tomar el riesgo de ignorar el principio open-closed. Llegará un momento de nuestra librería donde introducir cambios no compatibles hacia atrás ya no será tan sencillo.
Añadimos la modificación en los lugares mencionados más arriba. Por ejemplo, veamos la salida de git diff para nuestro interactor:
@@ -26,10 +27,8 @@ class SignUpInteractor implements SignUpInputPort
return;
}
- $userId = $this->userRepository->createUser(
- $request->username,
- $request->password
- );
+ $user = new User($request->username, $request->password);
+ $userId = $this->userRepository->createUser($user)Hasta ahora, hemos encapsulado la información del usuario en una clase. Aunque alguien pueda extender nuestra clase Usuario, la creación de instancias de la clase User sigue estando limitada por nuestra implementación. ¿Cómo permitimos que quien extienda la clase Usuario, pueda extender las posibilidades durante la creación del mismo? Aquí es donde entra en juego el patrón Factory. Este patrón nos permite delegar la creación de instancias a una clase separada, que a su vez puede ser extendida. Veremos en un ejemplo de esto al final del post. A continuación, presentaremos la versión inicial de nuestra Factory, que ubicaremos en la misma carpeta que nuestro interactor:
class SignUpUserFactory
{
public function createUserFromRequest(
SignUpRequest $request
): User {
return new User(
$request->username,
$request->password
);
}
}Ahora podemos modificar el interactor para utilizar la factory para crear una instancia de la clase Usuario:
class SignUpInteractor implements SignUpInputPort
{
public function __construct(
private UserRepository $userRepository,
private SignUpOutputPort $output,
private ?SignUpValidator $validator = new EmailValidator(),
private ?SignUpUserFactory $userFactory = new SignUpUserFactory()
) {
}
public function signUp(SignUpRequest $request): void
{
...
$user = $this->userFactory->createUserFromRequest($request);
$userId = $this->userRepository->createUser($user);
...Hemos dotado a nuestro interactor de la capacidad de utilizar una factory para crear los usuarios. Quienes quieran crear un usuario con información adicional, podrán modificar el request, la factory y el propio User, como veremos en la siguiente sección.
Vamos a ver un caso donde utilizamos la librería para registrar un usuario que, además de username y password tendrá un avatar, consistente en una url (que deberemos validar también).
Primero, vamos a definir cómo queremos que sea el usuario a registrar:
class UserWithAvatar extends User
{
public function __construct(
private string $username,
private string $password,
private string $avatar
) {
}
}Luego un validador para asegurar que el avatar sea una url:
...
$validator = new class implements SignUpValidator {
public function validate(SignUpRequest $request): ValidationResult
{
$validationResult = new ValidationResult();
...
// validate that avatar is an url
if (!filter_var($request->avatar, FILTER_VALIDATE_URL)) {
$validationResult->addError(
'avatar',
"{$request->avatar} is not a valid URL"
);
}
return $validationResult;
}
};y creamos una factory que se ajusta al requerimiento de tener usuarios con avatars:
$factory = new class extends SignUpUserFactory {
public function createUserFromRequest(SignUpRequest $request): User
{
return new User(
$request->username,
$request->password,
$request->avatar
);
}
};Como se puede apreciar, el request ahora debería tener un atributo con el avatar. Para ello necesitamos extender la clase SignUpRequest que viene con la librería:
class SignUpRequestWithAvatar extends SignUpRequest
{
public string $avatar;
public function __construct(
string $username,
string $password,
string $avatar
) {
parent::__construct($username, $password);
$this->avatar = $avatar;
}
}Finalmente podemos instanciar el interactor con los elementos que hemos creado:
$signUpUseCase = new SignUpInteractor(
$userRepository,
$somePresenter,
$validator,
$factory
);
$validRequest = new SignUpRequestWithAvatar (
'user@example.com',
'some-password',
'https://example.com/avatar.jpg'
);
$signUpUseCase->signUp($validRequest);El código funcional del ejemplo está en el archivo user-with-avatar.php, dentro de la carpeta examples, junto con las clases que hemos creado.
Este es el enlace al código final tras lo visto en este post.
En este post hemos añadido una factory cuya responsabilidad es instanciar objetos de la clase User. Permitiendo extender esta factory, los usuarios de la librería podrán abarcar más escenarios de Sign Up. En el próximo post vamos a incorporar el caso de uso Login. También dejaremos de utilizar el password en formato plano dentro de la clase User.
¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.
Cerrar