Traits en PHP. Segunda parte.
CategoriasProgramación

Traits en PHP. Segunda parte

Traits en PHP. Segunda parte

Este artículo pretende ser una segunda parte del que escribí hace ya algún tiempo (bastante) por aquí.

En esta segunda parte hablamos de la posibilidad de usar métodos abstractos en los traits, así como de modificar la visibilidad de los métodos, precedencia u orden de preferencia, uso de propiedades, métodos estáticos y traits compuestos, es decir, los Traits también pueden hacer uso de otros Traits.

En la primera parte del artículo sobre Traits en PHP, vimos cómo los Traits nos permiten reutilizar código en múltiples clases sin necesidad de herencia.

Si recordamos bien, el objetivo de los traits no es otro que agregar funcionalidad reutilizable.

Métodos Abstractos en Traits

Aunque los Traits no pueden ser instanciados por sí mismos, pueden definir métodos abstractos que deben ser implementados por la clase que los use.

¿Recordamos que es un método abstracto?

En el ejemplo siguiente, queremos que cualquier clase que use el trait tenga que definir cómo se guarda un mensaje (puede ser en un archivo, base de datos, etc.), pero todos tendrán un método común llamado informar() que reutilizará esa lógica.

trait Mensajero
{
    // Método abstracto: la clase que use este trait debe implementarlo
    abstract public function guardarMensaje(string $mensaje);

    // Método común que llama al método abstracto
    public function informar(string $texto)
    {
        $this->guardarMensaje("INFO: " . $texto);
    }
}

class Notificador
{
    use Mensajero;

    // Aquí definimos cómo se guarda el mensaje
    public function guardarMensaje(string $mensaje)
    {
        echo "Guardando mensaje: $mensaje"; // por ejemplo en un json, base de datos, etc.
    }
}

$notificador = new Notificador();
$notificador->informar("El sistema está activo.");

La clase Notificador tiene automáticamente el método informar() gracias al trait Mensajero, y ese método puede usarse normalmente:

$notificador = new Notificador();
// Al estar incluido el trait 'Mensajero', la clase 'Notificador' tiene disponibles todos los métodos del trait.
$notificador->informar("El sistema está activo.");

Pero como el método guardarMensaje() es abstracto en el trait, es obligatorio que la clase que use ese trait lo implemente. Si no lo hace, PHP lanza un error fatal al intentar usar esa clase.

Visibilidad de Métodos

Al igual que sucede con una clase normal, PHP nos permite cambiar la visibilidad de los métodos importados desde un Trait en la clase que lo usa. Esto es útil si queremos hacer que un método definido como public en un Trait sea protected o private en la clase.

De esa manera, evitamos modificar el trait (lo cual podría afectar a otras clases que lo usan).

Para ajustar la visibilidad, lo hacemos directamente en el use del trait:

<?php

trait ValidacionesUsuario
{
    // Este método se define como público en el trait
    public function validarEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

class RegistroUsuario
{
    use ValidacionesUsuario {
        validarEmail as private; // Lo hacemos privado dentro de esta clase
    }

    public function registrar(string $nombre, string $email)
    {
        if (!$this->validarEmail($email)) {
            echo "Correo no válido.";
            return;
        }

        // Aquí iría la lógica para guardar el usuario...
        echo "Usuario '$nombre' registrado con éxito con el correo $email.";
    }
}

$registro = new RegistroUsuario();
$registro->registrar("Óscar", "oscar@example.com"); // Correcto

Como vemos, el trait ValidacionesUsuario define validarEmail() como público y en la clase RegistroUsuario, lo hacemos private al incluirlo con as private dentro del use.

De esta manera, solo RegistroUsuario puede usarlo internamente.

Si hiciéramos esto:

$registro->validarEmail("algo"); // Error: método privado

Nos daría un error:

Fatal error: Uncaught Error: Call to private method RegistroUsuario::validarEmail() from global scope…”

Sencillamente porque el método es privado en esta clase.

Precedencia u orden de preferencia

¿Qué trait es el más chulo? ¿Cuál prefieres? Nah, fuera coñas!

Con el orden de preferencia queremos decir que, si una clase usa varios Traits que contienen métodos con el mismo nombre, PHP seguirá unas normas o regla de precedencia:

  • Si la clase implementa un método con el mismo nombre, tiene prioridad sobre el Trait.
  • Si múltiples Traits tienen un método con el mismo nombre, el último Trait incluido tiene prioridad.

Esto, ya lo expliqué en el primer artículo que escribí sobre la introducción a los traits, peeeero, haciendo un poco de autocrítica, debo admitir que le falto un poco más de… “chicha” (como dice un amigo mío), por eso lo intento explicar otra vez, pero mejor 😉

Atención al siguiente código:

<?php

trait A
{
    public function saludo()
    {
        echo "Hola desde A";
    }
}

trait B
{
    public function saludo()
    {
        echo "Hola desde B";
    }
}

class Saludar
{
    use A, B {
        B::saludo insteadof A;
    }
}

$obj = new Saludar();
$obj->saludo(); // "Hola desde B"

Si la clase Saludar tuviera también un método llamado saludo(), el que se ejecutaría sería el de la clase. Ahora bien, al haber dos traits con el mismo nombre de método, ¿Cuál se ejecuta?

Si lo ejecutamos tal cual nos daría error, es decir, si hiciéramos:

class Saludar
{
    use A, B;
}

$obj = new Saludar();
$obj->saludo();

Nos lanzaría un error.

Para evitarlo tenemos que hacer lo que hicimos antes, usar insteadof indicándole qué método de qué trait queremos usar. Porque sí, aunque la regla de preferencia de PHP diga que:

“Si múltiples Traits tienen un método con el mismo nombre, el último Trait incluido tiene prioridad.”

PHP no puede saber cuál es el que realmente deseas usar, y prefiere que lo digas explícitamente usando insteadof.

Así que debemos usar insteadof o, si queremos acceder a los dos métodos, bastaría con renombrar uno con as:

class Saludar
{
    use A, B {
        B::saludo insteadof A;
        A::saludo as saludoDesdeA;
    }
}

$obj = new Saludar();
$obj->saludo(); // "Hola desde B"
$obj->saludoDesdeA(); // "Hola desde A"

Y después de todo esto… podrías pensar… 🤔​

Y si hacemos así:

class Saludar {
    use A, B {
        A::saludo as saludoA;
        B::saludo as saludoB;
    }
}

Pues déjame decirte que seguirá dando error.

¿Por qué? Porque, aunque los renombres son válidos, no eliminan el conflicto de nombres original.

Es decir, el método original (saludo) sigue existiendo, entonces, si alguien ejecuta $obj->saludo(), ¿cuál de los dos métodos debe ejecutar PHP?

Por eso la solución es usar insteadof para eliminar el conflicto, y luego si poder renombrar ambos:

class Saludar {
    use A, B {
        A::saludo insteadof B;
        A::saludo as saludoA;
        B::saludo as saludoB;
    }
}

Con eso, si alguien llama directamente a saludo(), se usará el de A. Y por otra parte, saludoA() y saludoB() también estarán disponibles con sus respectivos métodos.

Así que, resumiendo, as no resuelve conflictos. Solo crea alias adicionales. Y para conflictos, siempre necesitamos usar insteadof para dejar claro a PHP cuál debe prevalecer. ¿Aclarado? 😉

Uso de propiedades

En los traits también se pueden definir propiedades:

<?php

trait CodigoIdentificador
{
    public $codigo = 0;
}

class Producto
{
    use CodigoIdentificador;
}

$producto = new Producto();
$producto->codigo = 123;

echo "Código del producto: " . $producto->codigo;

Aunque como indica la documentación oficial de PHP y cito textualmente:

“Si un trait define una propiedad, una clase no puede definir una propiedad con el mismo nombre, a menos que sea compatible (misma visibilidad y mismo valor inicial), si no, se emitirá un error fatal. Antes de PHP 7.0, definir una propiedad en una clase con la misma visibilidad y el mismo valor inicial que en el rasgo, emitía un aviso E_STRICT.”

O sea que, si tenemos un trait que define una propiedad (por ejemplo, $codigo), y la clase que usa ese trait también define una propiedad llamada igual, PHP nos lanzará un error.

Por ejemplo:

<?php

trait CodigoIdentificador
{
    public $codigo = 0;
}

class Producto
{
    use CodigoIdentificador;

    public $codigo = 123;
}

$producto = new Producto();

echo "Código del producto: " . $producto->codigo;

Nos muestra:

Fatal error: Producto and CodigoIdentificador define the same property ($codigo) in the composition of Producto. However, the definition differs and is considered incompatible.”

Porque, aunque el nombre y la visibilidad son iguales (public $codigo), el valor inicial es diferente (0 y 123).

Si queremos definir dos propiedades iguales, deben ser exactamente compatibles, es decir:

  • Mismo nombre
  • Misma visibilidad (public, protected o private)
  • Mismo valor inicial

Si no se cumplen las tres condiciones, PHP lanzará un error fatal.

Así que:

<?php

trait CodigoIdentificador
{
    public $codigo = 0;
}

class Producto
{
    use CodigoIdentificador;

    public $codigo = 0;
}

$producto = new Producto();

echo "Código del producto: " . $producto->codigo;

Aquí no pasa nada porque todo es idéntico: nombre, visibilidad y valor inicial, lo cual, ¡todo correcto!

Métodos estáticos

Los Traits pueden definir métodos estáticos, los cuales pueden ser llamados sin instanciar la clase.

Es igual que un método estático de una clase normal.

<?php

trait Formateo
{
    public static function formatearTexto($texto)
    {
        return strtoupper(trim($texto));
    }
}

class Mensaje
{
    use Formateo;
}

echo Mensaje::formatearTexto('  hola, soy oscar de oscardev.net  ');

Como vemos, llamamos a Mensaje::formatearTexto() sin necesidad de crear un objeto.

El resultado es simple:

Primero elimina los espacios (trim) y luego convierte todo a mayúsculas (strtoupper) mostrándose: “HOLA, SOY OSCAR DE OSCARDEV.NET”

En fin, no tiene ningún misterio jeje 😅​

Traits compuestos

Igual que las clases, los Traits pueden utilizar otros Traits, lo que nos permite estructurar mejor el código y reutilizar funcionalidades de manera modular.

<?php

trait A
{
    public function metodoDeA()
    {
        echo "Método de A";
    }
}

trait B
{
    use A;
    public function metodoDeB()
    {
        echo "Método de B";
    }
}

class Ejemplo
{
    use B;
}

$obj = new Ejemplo();
$obj->metodoDeA(); // "Método de A"
$obj->metodoDeB(); // "Método de B"

Como vemos, el trait compuesto sería el trait B, ya que B utiliza A, por lo que cualquier clase que use B también obtiene los métodos de A.

Otro ejemplo más detallado y más completo es el siguiente:

<?php 

trait Log
{
    public function escribirLog($mensaje)
    {
        echo "[LOG]: " . $mensaje . PHP_EOL;
    }
}

trait Error
{
    public function escribirError($mensaje)
    {
        echo "[ERROR]: " . $mensaje . PHP_EOL;
    }
}

trait SistemaMensajes
{
    use Log, Error;
}

class Aplicacion
{
    use SistemaMensajes;
}

$app = new Aplicacion();
$app->escribirLog('Aplicación iniciada.');
$app->escribirError('Se produjo un error.');

Como podemos observar:

  • Log y Errores son traits independientes.
  • SistemaMensajes es un trait compuesto, que usa a Log y Errores.
  • Y Aplicacion simplemente usa SistemaMensajes, que automáticamente tiene disponibles los métodos escribirLog() y escribirError().

Gracias a los traits compuestos, podemos organizar mejor nuestro código agrupando funcionalidades de forma organizada y reutilizable.

Resumiendo, los Traits en PHP son una herramienta poderosa para la reutilización de código.

Y ahora sí, finalizamos aquí el articulo y espero, como siempre, que hayas encontrado lo que buscabas y te haya sido útil.

Por cierto, ¿se te ocurren otras formas o aplicaciones interesantes para los Traits en PHP?

¡Déjamelo saber en los comentarios! 😜​

Sobre el autor

Comparte:

Este artículo está publicado bajo una licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional . Puedes compartirlo y adaptarlo, incluso con fines comerciales, siempre que cites al autor y mantengas esta misma licencia.

Deja una respuesta

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

Información básica sobre protección de datos
Responsable Óscar Martínez +info...
Finalidad Gestionar y moderar los comentarios +info...
Legitimación Consentimiento del interesado. +info...
Destinatarios No se cederán datos a terceros, salvo obligación legal +info...
Derechos Acceder, rectificar y cancelar los datos, así como otros derechos. +info...
Información adicional Puedes consultar la información adicional y detallada sobre protección de datos en nuestra página de política de privacidad.

Este sitio esta protegido por reCAPTCHA y laPolítica de privacidady losTérminos del servicio de Googlese aplican.

El periodo de verificación de reCAPTCHA ha caducado. Por favor, recarga la página.