Laravel es un framework bastante sencillo de usar, pero hay varios conceptos que a veces son difíciles de captar y tienen mucho potencial. Uno de ellos es las Facades que, a veces, son complicados de entender.

De forma resumida y simplificada una Facade nos permite acceder con métodos “estáticos” a clases registradas en el contenedor de nuestra aplicación tal y como puedes ver en el siguiente ejemplo:

Route::get('/cache', function () {
	return "Hola <3!";
});

En realidad, en las líneas de código anteriores, el método get no está definido en Illuminate\Support\Facades\Route sino que lo encontramos en la clase Illuminate\Routing\Router en dónde el método ni siquiera es estático.

class Router implements BindingRegistrar, RegistrarContract
{
    public function get($uri, $action = null)
    {
    	return $this->addRoute(['GET', 'HEAD'], $uri, $action);
    }
    
    // ...
}
Método get en la clase Router

¿Por qué queremos usar métodos estáticos?

Cómo sabrás, los métodos estáticos tienen ventajas, como que podemos llamarlos desde cualquier sitio sin inyectar ninguna clase y sin preocuparnos por nada y desventajas como que, a la hora de escribir un tests, son casi imposible de mockear y por tanto dificultan la tarea de probar partes aisladas del código.

Las Facades de Laravel solucionan esto como vamos a ver en el siguiente ejemplo.

Supongamos que tenemos un modelo Workout que es complejo de crear. Para ello normalmente usaríamos un servicio que, dependiendo de una configuración, crearía paso a paso este entrenamiento. En nuestro caso se va a llamar WorkoutGenerator y para simplificarlo todo, la configuración va a ser un simple array. Sería algo así:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Workout extends Model
{
    //
}
<?php

namespace App\Services;

class WorkoutGenerator
{
    public function generate(array $config): Workout
    {
    	// ...
        
        return $workout;
    }
}

El código anterior es perfectamente válido y podríamos inyectar el servicio en cualquier controlador y simplemente crear el entrenamiento llamando al método generate y pasándole la configuración deseada para dicho entrenamiento, pero molaría poder hacer algo así Workout::generateWithConfig([...]) y que esto nos devolviera un entrenamiento válido sin perder la testabilidad del código anterior. Eso podemos lograrlo gracias a las Facades de Laravel.

¿Cómo crear una Facade de Laravel?

Lo primero que hay que hacer es crear un ServiceProvider que instancie el servicio para el que queremos crear la Facade.

<?php

namespace App\Providers;

use App\Services\WorkoutCreator;
use Illuminate\Support\ServiceProvider;

class WorkoutCreatorProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(\App\Facades\WorkoutCreator::NAME, function ($app) {
            return $app->make(WorkoutCreator::class);
        });
    }
}

Como vemos, en la línea 12, estamos registrando un string que usamos también en la siguiente Facade para indicarle a esta última que servicio debe instanciar al llamarla.

<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class WorkoutCreator extends Facade
{
    const NAME = 'WorkoutCreator';
    
    protected static function getFacadeAccessor()
    {
        return self::NAME;
    }
}

Simplemente con esto, tras ejecutar composer dump-autoload para refrescar, podremos añadir lo siguiente a nuestro modelo de Workout anterior.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Workout extends Model
{
	public static function createWithConfig(WorkoutConfig $workoutConfig): Workout
    {
        return WorkoutCreator::withConfig($workoutConfig)->create();
    }
}

En este caso el propio método del modelo también es un método estático, pero es simplemente por que es un create, pero  no sería necesario.

Y  bien, ahora te estarás preguntando,

¿Cómo testeamos las Facades?

Aunque nosotros no lo vemos, detrás de una Facade ya tenemos muchas utilidades para mockearlas y testarlas sin prácticamente tener que hacer nada. En el siguiente ejemplo vamos a ver como podríamos testear el modelo para asegurarnos que se llama a la Facade tal y como esperamos.

class WorkoutTest extends TestCase
{
    public function testCreateWithConfig()
    {
        $workoutConfig = new WorkoutConfig();
        
        WorkoutCreator::shouldReceive('withConfig')->once()
            ->with($workoutConfig)
            ->andReturnSelf();
        WorkoutCreator::shouldReceive('create')->once();
        
        $workout = Workout::createWithConfig($workoutConfig);
    }

Cómo vemos, estamos logrando aislar el modelo y probando que únicamente se llaman a los métodos que queremos sin ejecutar realmente el código que hay en el servicio a pesar de que estamos llamando a este con método estáticos.

Luego, crearíamos otros tests unitarios normales para nuestro servicio, igual que hacemos siempre, y ya nos estaríamos asegurando de que tanto el servicio como el modelo hacen lo que esperamos.


¡Escríbeme por twitter ahora mismo y hablamos sobre esto! ¿Alguna duda?  ¿alguan  sugerencia?

Respondo siempre 😉