El método mágico invoke en PHP. La función aliada en el clean code

Los métodos mágicos de PHP hacen la vida del programador más fácil. Hoy te voy a explicar porqué el método mágico __invoke es tu aliado en multitud de casos y te va a permitir escribir un código más legible y mantenible. Y te lo demuestro con ejemplos.
El método mágico invoke en PHP. La función aliada en el clean code

El método __invoke forma parte de los métodos mágicos y de PHP desde la versión 5.3, hace un par de años ya. Sin embargo, aunque otros métodos mágicos (como toString) están muy presentes, __invoke no se suele ver mucho en código ajeno. Supongo que por desconocimiento general 🤔. Vamos a intentar que eso no ocurra; permíteme presentarte algunos casos extremadamente útiles para el uso del método __invoke y porqué es tremendamente útil cuando quieres hacer código limpio (clean code, que si no sabes lo que es, básicamente es entender un código de un vistazo sin pensar "WTF?...")

Qué es __invoke en PHP

El método inkove es invocado cada vez que se llama a un objeto como si se tratase de una función. Es decir, en otras palabras, este método se llama cada vez que escribimos lo siguiente...

<?php
class SaludaClass
{
    public function __invoke($x)
    {
        return "Hola $x"
    }
}
$obj = new SaludaClass;
$obj("yo");

Lo que estamos haciendo aquí es crear un método por defecto que se llame cuando tratamos el objeto de PHP como si fuese una función. El uso de la sintaxis $objeto() hace que se active __invoke.

Esto nos permite principalmente dos cosas. Por otro lado nos fuerza a seguir la S de SOLID. Al fin y al cabo, si una clase tiene solo una responsabilidad, porque hacer más de un método público. Es más, porqué si la clase solo tiene una sola responsabilidad, solo definimos el método público como invoke. Por otro lado, reducimos la cantidad de código que leemos y por tanto mejorar la legibilidad. Fíjate en que no es necesario usar la sintaxis general de $objeto->método();

De esta forma, sólo hay que buscar un buen nombre para la clase, que permita definir bien qué hace. Ya no es necesario buscar también un nombre para la función (que al final va a ser redundante).

Usar una función o usar __invoke

Partamos de la base que, según SOLID y mas concretamente la S, cada unidad de codigo tiene una sola responsabilidad. Esto significa que una clase o función debería resolver una sola tarea. Si por ejemplo tenemos que calcular un dato y luego en base a ese dato, enviar por mail, realmente hay tres clases o funciones asociadas: la que obtiene el dato, la que lo compara y la que envía el mail.

Imagina que necesitamos realizar un proceso que se repite a lo largo de nuestro código, un proceso más o menos simple como obtener un valor en base a otro. Pongamos por ejemplo determinar si un usuario es residente en Madrid o no a partir de un código postal (que por sencillez, se guarda como string).

Volviendo a nuestro caso, podríamos tener una función o clousure tal que

function viveEnMadrid(User $user) {
  return substr($user->codigoPostal, 0, 2) === "28";
}

Y esto en sí no está mal; es un ejemplo práctico bueno. Dado que el código anterior no tiene dependencias y a partir del objeto pasado como parámetro podemos calcular el valor deseado. Simplemente funciona ¯_(ツ)_/¯.

Sin embargo, qué pasaría si a lo largo del proceso, necesitásemos alguna dependencia. Pongamos que ahora necesitamos una función para saber si un usuario es mayor de edad a partir de los años que tiene. Como (insisto) intentamos ser buenos programadores y como es posible que nuestra aplicación se use en varios países, no deberíamos usar un literal y realizar una comparación con 18 años (mayoría legal en España). Por ende, necesitamos un comparador. De esta forma, le pasaremos la edad a nuestro comparador y él (en base a la ubicación del usuario por ejemplo) calcule si es mayor de edad.

Podría quedar algo así.

function esMayorDeEdad(ValidadorEdadLegal $validador, User $user): bool {
    return $validador->validarEdadLegal($user->edad);
}

No acaba de ser bonito. Si tenemos que instanciar la dependencia de nuestra función o clase fuera de la propia entidad, se rompe la encapsulación y abstracción. Es el usuario quien tiene que instanciar el comparador fuera de la propia unidad de código y estamos delegando en él la responsabilidad.

Esto ocurre porque las funciones o métodos de PHP no admiten la inyección de dependencias externas como tal. Por ello, habría que recurrir a una clase (o a un clousure, que junto con el fuctor, si permitirá una inyección externa de dependencia pero rompería nuestra abstracción y encapsulación).

Por lo tanto, en funciones no podemos usar el método __invoke ni nada que se le parezca, pues es un método mágico aplicado solo a objetos; instancias de clases. Si bien nuestro código es sencillo y no tiene dependencias, podemos usar una función. Si por el contrario sí que tiene dependencias, no deberíamos usar funciones.

Usando una clase con __invoke

Cuando una función no puede hacer algo, siempre podemos recurrir a una clase. Creamos una clase que defina un constructor en el que se inyecte nuestro validador. Dentro de esta clase, que se llama ValidadorEdadLegal, una función que reciba el objeto $user. Esta función pública se llamaría... ¿prácticamente igual que la clase? ¿validarEdadLegal()?. Y para invocarla, habría que escribir

function ... (ValidadorEdadLegal $validador) {
    $validador->validarEdadLegal(x)
}

Estamos escribiendo dos veces el mismo concepto, uno para la clase y otro para el método. Cuando se elije un buen nombre para la clase, es prácticamente autoexplicativo. Esto quedaría más simple y más legible con el método invoke.

function .... (ValidadorEdadLegal $validador) {
    $validador(x)
}

Analicemos las ventajas del código anterior.

  • Es simple de leer. Sin pararnos a mirar en cómo funciona la clase por dentro, sabemos que valida que algo o alguien tenga la edad legal.
  • Sumado a esto, no tenemos que ver el mapa de la clase para ver qué método público podemos usar. Siempre usamos invoke.
  • Si te acostumbras a usar invoke, cada vez te será más difícil declarar varios métodos por clase (lo que al final da a lugar a las terribles clases XManager o XServices, que aglutinan 2000 funciones).
  • Junto con Solid y CleanCode, los test te quedarán también más legibles.

¿ Y cuándo lo tengo que usar con $this ?

class X() {
    public function __construct(ClaseConInvoke $x) {
      $this->$x = $x;
    }

    public function myMethod() {
      //¿ Cómo llamo al __invoke de ClaseConInvoke x ?
      $this->x(); // ❌

      // Mejor así ...
      $this->x->__invoke(); // ✅

    }
}

Las primeras veces que recurras al método mágico __invoke de una clase A y tengas que usarlo en otra clase B junto con this (porque la has inyectado como dependencia), te surgirá la duda: ¿como la invoco? Normalmente usamos $this->algo->método(), pero ¿con invoke funciona igual?

Cuando inyectamos la clase con __invoke definido, podemos usarlo como $objeto(), pero cuando lo necesitamos llamar con this, no funciona exactamente igual.

Si usamos $this->x(), PHP intentará buscar un método llamado x en la propia clase y no lo encontrará. De alguna forma hay que hacerle saber que no estamos buscando un método en la propia clase sino que estamos llamando al método mágico __invoke de otra. Esto lo conseguimos con:

$this->x->__invoke($params);

Conclusión. Invoke está para ayudar

Como decía al principio de este artículo, no he visto de forma tan cotidiana, como __constrcut o __toString, el método invoke en el código de otros programadores. Es posible que parte de ellos no conocieran este útil método. Pero desde luego, desde que lo conozco, lo intento usar todo lo posible por las ventajas que he comentado a lo largo de este documento.

Espero haberte convencido de que el método mágico invoke está desde PHP 5.3 para hacernos la vida más fácil y dar más legibilidad al código. Al fin y al cabo, con pensar un buen nombre para la clase, basta. Piensa que en informática solo hay dos cosas realmente difíciles: pensar un buen nombre para algo e invalidar la caché: para que vamos a pensar dos nombres buenos pudiendo pensar solo uno.