No sé si alguna ves te ha pasado, que con el tiempo tus vistas a pesar de que las organices para hacerlas mas manejables, llegan a un punto donde comienzan a tener mucha lógica y comiences a preguntarte si es posible mejorarlas de alguna forma.
Pues bien si es posible y no estas solo,
Hace poco mas de una semana atrás, tuvimos un charla sobre este tema en nuestro slack y platicamos sobre como puedes tener vistas mas ligeras moviendo parte de su lógica a algo que se conoce como Presenter.
Esta estrategia no es nueva, pero puede ser muy útil conocer su uso. Y si no te suena el nombre no te preocupes que vamos a ver de que se trata y como podemos sacarle provecho.
Qué son los Presenter
En realidad el Presenter es parte de un modelo de arquitectura llamado Model View Presenter que es una variante del modelo MVC.
El Presenter actúa sobre el modelo y la vista. y su función es obtener datos del modelo para formatearlo para que los presente la vista sin tener que usar lógica adicional.
¿Que dijo?
si esto parece confuso, no te preocupes vamos a verlo con mas detalle.
Como funciona este patrón
Si pongo esto en un diagrama, el Presenter se ve algo como esto:
Bien veamos como funciona esto del Presenter:
vamos a suponer que hacemos un Request a la siguiente ruta
http://domain/users/1
En nuestro Resource Controller esa petición se procesa de la siguiente forma
1 2 3 4 | public function show(User $user) { return view('user.show', ['user' => $user]); } |
Como estoy utilizando implicit binding obtengo el usuario y lo paso a la vista.
Pero espera, ¿donde esta el Presenter?
Sabía que te darías cuenta, el presenter como muestra el diagrama esta entre la vista y el controlador.
Así que corrigiendo nuestro ejemplo, quedaría como sigue
1 2 3 4 | public function show(User $user) { return view('user.show', ['user' => new Presenter($user)]); } |
Esto quiere decir que el Presenter es simplemente una clase que actúa como el modelo y que ademas puede pasar lógica adicional a la vista. que de otra forma terminaría en el modelo, el controlador o la vista,
donde no debe de estar!
Pero que te parece si dejamos la teoría y vamos a lo nuestro con un ejemplo.
Sigamos con el ejemplo
Vamos a suponer que la vista del ejemplo previo presenta los datos del usuario mediante un card.
Algo sencillo sin mucha complicación,
y cuando vamos a la vista esperando ver algo sencillo, pero nos topamos con este código
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @section('content') <div class="row"> <div class="col-sm-6 offset-4"> <div class="card" style="width: 20rem;"> @if($user->avatar == null) <img class="card-img-top " src="{{$user->sex == App\User::MALE ? asset('img/male_avatar.png') : asset('img/female_avatar.png') }}" alt="Card image cap"> @else <img class="card-img-top" src="{{asset('img/'.$user->avatar)}}" alt="Card image cap"> @endif <div class="card-body"> <h2 class="card-title text-primary">{{$user->name}}</h2> <h5 class="card-subtitle mb-2 text-muted text-success">{{$user->email}}</h5> <h6>registered since: <small class="text-muted">{{$user->created_at->diffForHumans()}}</small></h6> <p class="card-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci architecto atque dicta dolor dolore eaque est exercitationem iste magni, molestias nulla obcaecati perspiciatis quis quod sapiente, tenetur veritatis voluptatum? Repudiandae?</p> </div> </div> </div> </div> @endsection |
Tres cosas puedes notar de este ejemplo
- La primera es que tenemos una serie de condiciones para establecer una imagen de acuerdo al sexo del usuario, cuando este no a subido una imagen.
- La segunda es la llamada a la clase App\User
- La tercera es la forma es que se esta llamando a la fecha de creación del usuario para darle formato
Ahora mi pregunta seria ¿Crees que esto debe de estar en la vista?
Pienso que la vista solo debiera de imprimir los datos sin complicaciones, en lugar de decidir cual imagen toca de acuerdo con el sexo del usuario.
Así que, me gustaría que la vista se viera de esta forma
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @section('content') <div class="row"> <div class="col-sm-6 offset-4"> <div class="card" style="width: 20rem;"> <img class="card-img-top" src="{{asset($user->getAvatar())}}" alt="Card image cap"> <div class="card-body"> <h2 class="card-title text-primary">{{$user->name}}</h2> <h5 class="card-subtitle mb-2 text-muted text-success">{{$user->email}}</h5> <h6>registered since: <small class="text-muted">{{$user->since()}}</small></h6> <p class="card-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci architecto atque dicta dolor dolore eaque est exercitationem iste magni, molestias nulla obcaecati perspiciatis quis quod sapiente, tenetur veritatis voluptatum? Repudiandae?</p> </div> </div> </div> </div> @endsection |
¿Qué te parece?
Ahora, ¿Cómo hacemos semejante maravilla?
Es mas sencillo de lo que te imaginas, ya que ahora sabemos que podemos usar un Presenter, solo tenemos que crearlo.
Cómo implementamos el Presenter
vamos a crear nuestra clase UserPresenter y en ella vamos a crear la logica para que funcione el método getAvatar()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | class UserPresenter { /** * @var User */ private $user; /** * @var array */ private $defaultAvatars = [ User::MALE => 'img/male_avatar.png', User::FEMALE => 'img/female_avatar.png', ]; /** * UserPresenter constructor. * * @param User $user */ public function __construct(User $user) { $this->user = $user; $this->setLocale(); } /** * @return mixed|string */ public function getAvatar() { if ($this->user->hasAvatar()) { return sprintf('img/%s', $this->user->avatar); } return $this->defaultAvatars[$this->user->sex]; } /** * Date localization */ public function setLocale() { Carbon::setLocale(config('app.locale')); } /** * Delegate calls to User model * * @param $method * @param $arguments * @return mixed */ public function __call($method, $arguments) { if (!method_exists($this->user, $method)) { throw new \BadMethodCallException("No such method {$method}"); } return call_user_func_array([$this->user, $method], $arguments); } } |
Como algo adicional agregue que se pueda establecer el idioma de acuerdo a la configuración de la aplicación (setLocale()).
Pero es muy probable que te preguntes para que sirve la parte que llama al método mágico __call y lo que hace es permitirme seguir llamando a los métodos y propiedades del modelo User sin tener que crear nuevos métodos en la clase UserPresenter.
De esta forma no tengo que agregar cambios adicionales.
Muy bien, ahora nos falta agregar el método since() y el método hasAvatar() que debiste de ver en la clase UserPresenter, pero estos dos métodos los he mandado al Modelo User.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class User extends Authenticatable { use Notifiable; const MALE = 1; const FEMALE = 2; //... /** * Verifies that the user has a custom image * * @return bool */ public function hasAvatar() { return $this->avatar != null; } /** * Prints the time when user has enrolled * * @return mixed */ public function since() { return $this->created_at->diffForHumans(); } } |
Estos dos métodos lo e creado aquí, porque creo que es donde deben de estar, si a tí no te gusta o crees que no deben de estar en ese lugar, los puedes pasar al Presenter no pasa nada.
Muy bien, ya solo nos falta hacer los cambios en el Controller como sigue.
1 2 3 4 | public function show(User $user) { return view('user.show', ['user' => new UserPresenter($user)]); } |
Y listo hemos terminado, ahora la vista luce mas legible, no tuviste que hacer muchos cambios y eres pura felicidad!
Pero….
¿Que pasa si tu modelo tienes relación con otro?
Si no lo habías pensado, este es un punto importante, ya que si el modelo relacionado tiene también un Presenter no lo vamos a poder usar!!
Mejoremos el Presenter
Pero tranquilo, que existen soluciones
Como no hemos visto nada de los decoradores esta opción la vamos a dejar por fuera.
Así que vamos a usar la siguiente, que sería lo opuesto que tenemos en este momento.
Es decir que el modelo delegue las peticiones que vayan hacia UserPresenter mediante el uso del método __call, de esta forma si tenemos relaciones las podemos usar sin problemas ademas de los métodos del presenter.
Para este caso mi elección sera mediante un Trait que se encargara de instanciar el UserPresenter y podamos usarlo desde el Modelo, en este caso desde User.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | trait HasPresentable { /** * Presenter instance * * @var mixed */ protected $presenterInstance; /** * Prepare a new presenter instance * * @return mixed * @throws \Exception */ public function present() { if ( ! $this->presenter) { throw new \Exception('define the $presenter property in your model'); } if(! class_exists($this->presenter)) { throw new \Exception("The presenter {$this->presenter} doesn't exist"); } if ( ! $this->presenterInstance) { $this->presenterInstance = new $this->presenter($this); } return $this->presenterInstance; } /** * Call presenter methods * * @param $method * @param $arguments * @return mixed * @throws \Exception */ public function __call($method, $arguments) { if (method_exists($this->present(), $method)) { return call_user_func_array([$this->present(), $method], $arguments); } return parent::__call($method, $arguments); } } |
Ya que movimos la llamada a __call, va ser necesario quitarlo del presenter, así que no olvides borrar esa parte!
En nuestro modelo User vamos a usar el trait de la siguiente forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | class User extends Authenticatable { use Notifiable; use HasPresentable; const MALE = 1; const FEMALE = 2; protected $presenter = UserPresenter::class; //... /** * Verifies that the user has a custom image * * @return bool */ public function hasAvatar() { return $this->avatar != null; } /** * Prints the time the user has enrolled * * @return mixed */ public function since() { return $this->created_at->diffForHumans(); } } |
Para finalizar tenemos que cambiar el controlador
1 2 3 4 | public function show(User $user) { return view('user.show', ['user' => $user]); } |
Listo, ahora ya todo funciona como debe de ser.
El resumen
Como puedes ver el Presenter puede ser muy util para reducir la complejidad de las vistas y puedes mantener el código de forma mas sencilla.
Pero debes de estar atento de mantener el equilibrio y pensar muy bien que lógica mueves al Presenter ya que puede terminar con lógica que no le pertenece.
También existen otras formas de implementar los presenter usando decoradores y puedes probar cosas mas al estilo Laravel implementado la interfaz Responsable.
Finalmente deja tus comentarios y no olvides compartir!