Un buen uso de Traits en Laravel

Profundizando en el uso que estoy haciendo de este framework, siempre me he preocupado por seguir las mejores prácticas. No simplemente copiar y pegar todo lo que pueda encontrar en StackOverflow sobre lo que son “mejores prácticas” si no aquéllo que para mi tenga sentido. Laravel no es un framework tan opinionated como lo pueda ser Ruby on Rails, lo que a mi parecer te deja más flexibilidad. Cualquier crítica será bienvenida ;)

Uno de los temas que llevaba tiempo pensando es donde poner funciones auxiliares que deban ser compartidas por los controladores y posiblemente también por los modelos. Normalmente cuando vamos construyendo la aplicación nos vamos encontrando con funcionalidades repetidas. Posiblemente no sean más que 2 o 3 líneas de código, pero eso ya es suficiente motivo para crear una función que las encapsule.

Supongamos que tenemos que convertir una fecha que viene desde un formulario en el navegador para adaptarla a formato UTC (una buena práctica en cualquier aplicación web) y además convertirla al formato que utilice la base de datos, que normalmente será YYYY-MM-DD HH:MM:SS, como es el caso de MySQL con su datetime. Si la aplicación web está en español, el usuario introducirá fechas en el formato DD/MM/YYYY en la zona horaria española, que será UTC+1 en el horario de invierno. Pero normalmente querremos guardar la fecha en Tiempo Universal Coordinado (UTC) por si hay que convertirla para cualquier otra localización.

La primera tentación será realizar las operaciones que hagan falta para realizar la conversión directamente en una de los métodos de un controlador. Pero ¿qué pasa si hay otro controlador del sistema que quiere realizar lo mismo? Código repetido y violación del principio DRY (Don’t repeat yourself).

Otra cosa que podemos hacer es crear un helper de Laravel. Hay gente que lo hace y os encontraréis páginas y blogs comentando las “mejores prácticas” para crear helpers. Personalmente, aunque esto funcione, no me parece una solución elegante. No veo nada en la documentación de Laravel que te empuje a la creación de tus propios helpers y creo que solo deberían definirse para extender las capacidades del framework, no para resolver un problema determinado de una aplicación. Me da la sensación de que no es el camino más correcto. Además, terminarías con un montón de funciones en el espacio global de la aplicación que no tendrían ninguna cohesión. En resumen, todo intento de solución de un problema en el que termine modificando el framework en lugar de extenderlo me parece digno de evitar.

Un poco más elegante es la solución de crear un Provider de Laravel. De hecho están para eso, para ofrecer servicios nuevos a tu aplicación. Se pueden crear métodos estáticos en el provider que de proporcionarán la funcionalidad requerida. Esto te proporciona la ventaja de una cierta cohesión de funciones relacionadas. Lo que no me termina de gustar es que estás usando una clase, no para modelar un objeto del mundo real, sino como una mera librería de funciones. Eso no es para lo que se crearon las clases.

Te presento a los Traits

Los Traits de PHP son precisamente lo que estábamos buscando. A partir de PHP 5.4.0 tenemos una forma de reutilización de código perfecta para la misión.

¿Qué es precisamente lo que estábamos buscando al principio? Necesitábamos una manera de extender las funcionalidades del controlador -en este caso realizar la conversión de fechas- pero sin tener que repetir el código, creando la función fuera de cualquier controlador y escribiéndola una sola vez. Vamos a ver como va esto. Vamos a crear el fichero Dates.php dentro del directorio app:

namespace App;
trait DatesTrait
{
    /**
    * Convierte la fecha a UTC y formato adecuado a la base de datos
    */
    public function dateToUTC($date_str)
    {
        //… algo de código genial
    }
}

Bien, ya tenemos listo el Trait (por cierto, se puede traducir por rasgo, característica, cualidad). Ahora supongamos que tenemos un controlador con un método que se encarga de dar de alta los bonos de descuento en un sistema de ventas online. El bono tendrá una fecha de caducidad que querremos guardar en formato UTC, ya que a la tienda se accede desde todos los países del mundo mundial. Lo que queríamos originalmente era que el controlador pudiera convertir la fecha, atención:

use App\DatesTrait;
class DiscountController extends Controller
{
    use DatesTrait;
    
    public function store(Request $request)
        $date_utc = $this->dateToUTC($request->expiration_date);
    //…más código del bueno
}

Lo que hemos conseguido usando los traits es interesante tanto desde el punto de vista funcional como desde el punto de vista semántico y de respeto al patrón MVC (Modelo Vista Controlador):

  • El Controlador es el encargado de recibir datos de la vista, procesarlos y actualizar el modelo, por tanto claramente es el encargado de esta tarea. Con los Traits hemos aumentado los superpoderes del controlador de una forma que será fácil de mantener en el futuro y podrá ser usada por todos los demás controladores.
  • Funcionalmente hemos conseguido lo que necesitábamos: convertir la fecha de una manera en la que no tuviéramos que repetir las mismas líneas de código cada vez que necesitáramos hacerlo.
  • Tampoco violamos el principio de única responsabilidad de las clases. Si pusiéramos el método que convierte fechas en un controlador cualquiera y luego lo invocáramos desde cualquier otro (instanciando manualmente el primer controlador) estaríamos haciendo que el controlador que contiene el método tuviera más de una responsabilidad. Ver SOLID
  • No creamos objetos para aquéllo para lo que no están pensados, refiriéndome a los Provider. Las clases están para modelar objetos del mundo real, con pocas excepciones y en principio no deben usarse como colección de funciones.

Seguiré compartiendo lo que vaya averiguando en cuestión de buenas prácticas con Laravel, el framework con el que estoy trabajando en PHP. ¿Tienes alguna manera distinta de resolver este problema?

Responder

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *