Facades

Què són les Facades de Laravel, com funcionen internament, Real-time Facades, testing i alternatives amb injecció de dependències.

Què són les Facades?#

Si has treballat amb Laravel encara que sigui una estona, ja has vist codi com Cache::get('key') o DB::table('users'). Aquestes crides semblen mètodes estàtics de classes normals, però no ho són. Són Facades: una capa elegant que proporciona una sintaxi curta i expressiva per accedir als serveis registrats al Service Container de Laravel.

La idea darrere de les Facades és simple però poderosa. En lloc d'haver d'injectar dependències al constructor o recuperar manualment serveis del contenidor, pots cridar un mètode directament sobre la Facade com si fos estàtic. Per darrere, Laravel resol el servei del contenidor i executa el mètode sobre la instància real. El resultat és un codi net i fàcil de llegir que manté tots els avantatges de la injecció de dependències:

use Illuminate\Support\Facades\Cache;
 
// Això sembla una crida estàtica...
Cache::put('user.1', $user, 3600);
$user = Cache::get('user.1');
 
// ...però per darrere és equivalent a:
$cache = app('cache');
$cache->put('user.1', $user, 3600);
$user = $cache->get('user.1');

Fixa't en la diferència: amb la Facade escrius una línia. Sense ella, primer has de recuperar el servei del contenidor i després cridar el mètode. El resultat és idèntic, però la Facade elimina el pas intermedi i fa el codi més directe.

La il·lusió de les crides estàtiques#

El concepte que genera més confusió quan es comença amb Laravel és que les Facades no són realment estàtiques. Quan escrius Cache::get('key'), no estàs cridant un mètode estàtic definit a la classe Cache. De fet, si obres la classe Illuminate\Support\Facades\Cache, no hi trobaràs cap mètode get(). Llavors, com funciona?

La resposta està al mètode màgic __callStatic() de PHP. Quan crides un mètode estàtic que no existeix a una classe, PHP busca si la classe té un mètode __callStatic() i l'executa com a fallback. La classe base Facade de Laravel implementa exactament aquest mètode, que s'encarrega de:

  1. Determinar quin servei del contenidor correspon a la Facade
  2. Resoldre la instància real del servei des del contenidor
  3. Cridar el mètode desitjat sobre la instància

Això significa que cada vegada que fas Cache::get('key'), en realitat estàs cridant app('cache')->get('key'). La Facade és un intermediari transparent que redirigeix la crida al servei real. L'objecte que processa la teva petició és una instància viva, amb estat i dependències resoltes, no una classe estàtica sense context.

Aquesta distinció és important per a la testabilitat. Les classes realment estàtiques són notòriament difícils de testejar perquè no es poden substituir ni simular (mock). Les Facades, en canvi, es poden testejar perfectament perquè per darrere són instàncies normals del contenidor. Laravel proporciona mètodes com fake() i shouldReceive() que substitueixen la instància del contenidor per un mock durant els tests.

Com funcionen les Facades internament#

Per entendre completament les Facades, val la pena examinar el mecanisme intern. Cada Facade de Laravel estén la classe base Illuminate\Support\Facades\Facade i implementa un únic mètode: getFacadeAccessor(). Aquest mètode retorna una cadena que identifica el servei al contenidor.

La classe base Facade#

La classe Illuminate\Support\Facades\Facade és el motor de tot el sistema. Conté la lògica per interceptar les crides estàtiques, resoldre serveis del contenidor i mantenir una cache d'instàncies resoltes. El mètode clau és __callStatic():

// Versió simplificada del que fa la classe base Facade
abstract class Facade
{
    // Cache d'instàncies resoltes
    protected static array $resolvedInstance = [];
 
    // Cada Facade ha d'implementar aquest mètode
    abstract protected static function getFacadeAccessor(): string;
 
    // Intercepta crides estàtiques a mètodes que no existeixen
    public static function __callStatic(string $method, array $args): mixed
    {
        $instance = static::getFacadeRoot();
 
        return $instance->$method(...$args);
    }
 
    // Resol la instància del contenidor
    public static function getFacadeRoot(): mixed
    {
        $name = static::getFacadeAccessor();
 
        // Si ja està resolta, retorna de la cache
        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }
 
        // Si no, resol del contenidor i cacheja
        return static::$resolvedInstance[$name] = app()[$name];
    }
}

La cache d'instàncies ($resolvedInstance) és un detall important. Quan crides Cache::get() per primera vegada, la Facade resol el servei del contenidor i el guarda. Les crides posteriors reutilitzen la mateixa instància sense tornar a consultar el contenidor, cosa que millora el rendiment.

El mètode getFacadeAccessor()#

Cada Facade concreta implementa getFacadeAccessor() per indicar quin servei del contenidor representa. Per exemple, la Facade Cache retorna la cadena 'cache', que és el nom amb què el servei de cache està registrat al contenidor:

namespace Illuminate\Support\Facades;
 
class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}

Quan escrius Cache::get('key'), el procés complet és:

  1. PHP detecta que Cache no té un mètode estàtic get() i crida __callStatic('get', ['key'])
  2. __callStatic() crida getFacadeRoot(), que crida getFacadeAccessor() i obté 'cache'
  3. getFacadeRoot() resol app('cache') del contenidor, obtenint la instància real del servei de cache
  4. __callStatic() crida $instance->get('key') sobre la instància real
  5. El resultat es retorna com si fos una crida estàtica normal

Tot aquest procés és transparent. Tu escrius Cache::get('key') i reps el valor. Però entendre el mecanisme intern t'ajuda a depurar problemes, crear Facades personalitzades i comprendre per què les Facades són testables.

Referència de Facade a servei#

Per saber quin mètode pots cridar sobre una Facade, necessites saber quina classe del contenidor hi ha per darrere. Cada Facade redirigeix a una classe concreta que conté els mètodes reals:

// Cache::get() realment crida Illuminate\Cache\CacheManager::get()
// DB::table() realment crida Illuminate\Database\DatabaseManager::table()
// Route::get() realment crida Illuminate\Routing\Router::get()
// Mail::to() realment crida Illuminate\Mail\Mailer::to()

Pots descobrir quina classe hi ha darrere d'una Facade amb el mètode getFacadeRoot():

// Obtenir la instància real darrere de la Facade
$cacheManager = Cache::getFacadeRoot();
echo get_class($cacheManager); // Illuminate\Cache\CacheManager

Facades comunes#

Laravel inclou dotzenes de Facades que cobreixen pràcticament tots els serveis del framework. Aquesta taula mostra les més utilitzades, l'accessor que retorna getFacadeAccessor() i la classe concreta que resol:

| Facade | Accessor | Classe resolta | |--------|----------|----------------| | Auth | auth | Illuminate\Auth\AuthManager | | Cache | cache | Illuminate\Cache\CacheManager | | Config | config | Illuminate\Config\Repository | | Cookie | cookie | Illuminate\Cookie\CookieJar | | Crypt | encrypter | Illuminate\Encryption\Encrypter | | DB | db | Illuminate\Database\DatabaseManager | | Event | events | Illuminate\Events\Dispatcher | | Gate | gate | Illuminate\Auth\Access\Gate | | Hash | hash | Illuminate\Hashing\HashManager | | Http | http | Illuminate\Http\Client\Factory | | Log | log | Illuminate\Log\LogManager | | Mail | mailer | Illuminate\Mail\Mailer | | Notification | notification | Illuminate\Notifications\ChannelManager | | Queue | queue | Illuminate\Queue\QueueManager | | Route | router | Illuminate\Routing\Router | | Schema | schema | Illuminate\Database\Schema\Builder | | Session | session | Illuminate\Session\SessionManager | | Storage | filesystem | Illuminate\Filesystem\FilesystemManager | | URL | url | Illuminate\Routing\UrlGenerator | | Validator | validator | Illuminate\Validation\Factory | | View | view | Illuminate\View\Factory |

Totes les Facades de Laravel viuen al namespace Illuminate\Support\Facades. Quan escrius use Illuminate\Support\Facades\Cache, estàs important la Facade, no el servei de cache en si. La Facade s'encarrega de redirigir les crides al servei real.

Facades vs injecció de dependències#

Un dels debats més habituals a la comunitat Laravel és: hauries d'usar Facades o injecció de dependències? La resposta curta és que les dues aproximacions són vàlides i cadascuna té els seus avantatges. La resposta llarga depèn del context.

Amb Facade#

Les Facades ofereixen conveniència i llegibilitat. El codi és directe, curt i no requereix configurar res al constructor. Són ideals per a operacions puntuals on injectar una dependència sencera seria excessiu:

namespace App\Http\Controllers;
 
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
 
class DashboardController extends Controller
{
    public function index()
    {
        $stats = Cache::remember('dashboard.stats', 3600, function () {
            return DB::table('orders')
                ->selectRaw('COUNT(*) as total, SUM(amount) as revenue')
                ->whereMonth('created_at', now()->month)
                ->first();
        });
 
        Log::info('Dashboard accedit', ['user' => auth()->id()]);
 
        return view('dashboard', compact('stats'));
    }
}

Amb injecció de dependències#

La injecció de dependències fa les dependències explícites. Quan mires el constructor d'una classe, veus immediatament de què depèn. Això facilita els tests (pots passar mocks al constructor) i fa el codi més fàcil d'entendre per a desenvolupadors que no coneixen Laravel:

namespace App\Http\Controllers;
 
use Illuminate\Cache\CacheManager;
use Illuminate\Database\DatabaseManager;
use Psr\Log\LoggerInterface;
 
class DashboardController extends Controller
{
    public function __construct(
        private CacheManager $cache,
        private DatabaseManager $db,
        private LoggerInterface $logger
    ) {}
 
    public function index()
    {
        $stats = $this->cache->remember('dashboard.stats', 3600, function () {
            return $this->db->table('orders')
                ->selectRaw('COUNT(*) as total, SUM(amount) as revenue')
                ->whereMonth('created_at', now()->month)
                ->first();
        });
 
        $this->logger->info('Dashboard accedit', ['user' => auth()->id()]);
 
        return view('dashboard', compact('stats'));
    }
}

Comparació#

La diferència pràctica entre les dues aproximacions és menor del que sembla. Amb Facades, les dependències són implícites: saps que el mètode utilitza cache, base de dades i logging perquè veus les crides a les Facades dins del mètode. Amb injecció de dependències, les dependències són explícites: les veus al constructor sense necessitat de llegir el cos del mètode.

Per a tests, les dues aproximacions funcionen bé. Les Facades tenen mètodes com fake() i shouldReceive() que permeten simular el servei sense tocar el contenidor manualment. La injecció de dependències permet passar un mock directament al constructor. En la pràctica, la diferència és mínima.

On la injecció de dependències brilla és en serveis complexos amb moltes dependències o quan vols programar contra interfícies. Si un servei depèn d'un PaymentGateway, és més clar veure-ho al constructor que trobar Payment::charge() enterrat al mig d'un mètode. Per a serveis del framework com cache, logs o base de dades, les Facades són perfectament adequades perquè tothom les reconeix immediatament.

Quan usar cada aproximació#

  • Facades: operacions del framework (cache, logs, sessions, mail), controladors simples, prototipatge ràpid, codi que ha de ser concís
  • Injecció de dependències: serveis de negoci personalitzats, classes amb dependències complexes, codi que segueix principis SOLID estrictes, paquets i llibreries reutilitzables

Facades vs funcions helper#

Laravel també proporciona funcions helper globals que fan el mateix que algunes Facades. Per exemple, cache() fa el mateix que Cache::get(), i session() fa el mateix que Session::get(). La diferència és purament estilística:

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Config;
 
// Amb Facades
$value = Cache::get('key');
$name = Session::get('name');
$debug = Config::get('app.debug');
 
// Amb helpers
$value = cache('key');
$name = session('name');
$debug = config('app.debug');
 
// Els helpers també permeten escriure valors
cache(['key' => 'value'], 3600);
session(['name' => 'Joan']);
config(['app.debug' => false]);

Els helpers són encara més concisos que les Facades i no requereixen cap use al principi del fitxer. Són ideals dins de vistes Blade o en contextos on vols el codi més curt possible. Les Facades, en canvi, són més explícites sobre quin servei estàs utilitzant i permeten que l'IDE ofereixi autocompletat complet dels mètodes disponibles.

La regla pràctica és simple: utilitza el que prefereixis i sigues consistent. No barregis Cache::get() i cache() al mateix fitxer sense un motiu clar. Molts desenvolupadors utilitzen helpers dins de Blade i Facades dins de controladors i serveis, cosa que és una convenció raonable.

Tant les Facades com els helpers criden exactament el mateix servei del contenidor. No hi ha diferència de rendiment ni de funcionalitat entre Cache::get('key'), cache('key') i app('cache')->get('key'). L'elecció és purament de preferència.

Real-time Facades#

Les Real-time Facades són una funcionalitat de Laravel que permet convertir qualsevol classe de la teva aplicació en una Facade al vol, sense necessitat de crear una classe Facade dedicada. Per fer-ho, simplement afegeixes el prefix Facades\ al namespace de la classe quan la importes.

Imagina que tens un servei personalitzat per a analítiques:

namespace App\Services;
 
class Analytics
{
    public function track(string $event, array $data = []): void
    {
        // Enviar l'event al servei d'analítiques
    }
 
    public function identify(int $userId, array $traits = []): void
    {
        // Identificar l'usuari
    }
}

Normalment, per usar aquest servei hauries d'injectar-lo al constructor o recuperar-lo del contenidor. Amb Real-time Facades, pots cridar-lo directament amb sintaxi de Facade:

use Facades\App\Services\Analytics;
 
class PageController extends Controller
{
    public function show(string $slug)
    {
        $page = Page::where('slug', $slug)->firstOrFail();
 
        // Crida com si fos una Facade
        Analytics::track('page.view', [
            'slug' => $slug,
            'title' => $page->title,
        ]);
 
        return view('pages.show', compact('page'));
    }
}

Quan Laravel veu Facades\App\Services\Analytics, genera automàticament una Facade al vol que resol App\Services\Analytics del contenidor. No cal crear cap classe Facade, ni registrar res al Service Provider, ni configurar cap alias. Simplement afegir Facades\ al principi del namespace és suficient.

Quan usar Real-time Facades#

Les Real-time Facades són útils per a prototipatge ràpid i per a serveis que no justifiquen una Facade completa. Si tens un servei que s'utilitza en dos o tres llocs i no vols crear una Facade dedicada, les Real-time Facades són una bona solució temporal. Però si el servei és central a la teva aplicació i s'utilitza a tot arreu, val la pena crear una Facade dedicada per tenir més control.

Un avantatge important de les Real-time Facades és que són automàticament testables amb shouldReceive():

use Facades\App\Services\Analytics;
 
public function test_page_view_is_tracked(): void
{
    Analytics::shouldReceive('track')
        ->once()
        ->with('page.view', \Mockery::type('array'));
 
    $this->get('/pages/about');
}

No abusis de les Real-time Facades. Si totes les teves classes s'accedeixen com a Facades, perds l'avantatge de la injecció de dependències explícita. Utilitza-les per a casos puntuals on la conveniència justifica la pèrdua d'explicitació.

Crear una Facade personalitzada#

Si tens un servei central que es fa servir extensivament a tota l'aplicació, crear una Facade dedicada ofereix més control que les Real-time Facades. El procés té tres passos: crear el servei, crear la classe Facade i assegurar-te que el servei estigui registrat al contenidor.

Pas 1: el servei#

Primer, crea la classe del servei que contindrà la lògica:

namespace App\Services;
 
class CurrencyConverter
{
    public function __construct(
        private string $baseCurrency = 'EUR'
    ) {}
 
    public function convert(float $amount, string $from, string $to): float
    {
        $rate = $this->getRate($from, $to);
 
        return round($amount * $rate, 2);
    }
 
    public function getRate(string $from, string $to): float
    {
        // Obtenir la taxa de canvi des d'una API o cache
        return Cache::remember(
            "exchange_rate.{$from}.{$to}",
            3600,
            fn () => $this->fetchRateFromApi($from, $to)
        );
    }
 
    public function format(float $amount, string $currency): string
    {
        return number_format($amount, 2, ',', '.') . ' ' . $currency;
    }
 
    private function fetchRateFromApi(string $from, string $to): float
    {
        // Crida a l'API de canvi
        $response = Http::get("https://api.exchangerate.host/convert", [
            'from' => $from,
            'to' => $to,
        ]);
 
        return $response->json('result');
    }
}

Pas 2: registrar el servei al contenidor#

Registra el servei al Service Provider. Utilitza singleton si vols una única instància compartida (el més habitual per a serveis d'aquest tipus):

// app/Providers/AppServiceProvider.php
namespace App\Providers;
 
use App\Services\CurrencyConverter;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton('currency', function ($app) {
            return new CurrencyConverter(
                baseCurrency: config('services.currency.base', 'EUR')
            );
        });
    }
}

Pas 3: la classe Facade#

Crea la classe Facade que apunti al servei registrat. L'únic que necessita és el mètode getFacadeAccessor() que retorni el nom amb què has registrat el servei al contenidor:

namespace App\Facades;
 
use Illuminate\Support\Facades\Facade;
 
/**
 * @method static float convert(float $amount, string $from, string $to)
 * @method static float getRate(string $from, string $to)
 * @method static string format(float $amount, string $currency)
 *
 * @see \App\Services\CurrencyConverter
 */
class Currency extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'currency';
    }
}

Els comentaris @method al PHPDoc són opcionals però molt recomanables. Permeten que l'IDE ofereixi autocompletat i documentació dels mètodes disponibles, ja que la Facade en si no conté cap mètode. L'anotació @see indica quina classe real hi ha darrere de la Facade, cosa que facilita la navegació del codi.

Usar la Facade#

Ara pots usar la Facade a qualsevol lloc de l'aplicació:

use App\Facades\Currency;
 
// Convertir moneda
$euros = Currency::convert(100, 'USD', 'EUR');
 
// Obtenir la taxa de canvi
$rate = Currency::getRate('USD', 'EUR');
 
// Formatar un import
echo Currency::format(49.99, 'EUR'); // "49,99 EUR"

Opcional: registrar un alias global#

Si vols poder usar Currency sense el use App\Facades\Currency complet, pots registrar un alias al fitxer config/app.php:

// config/app.php
'aliases' => Facade::defaultAliases()->merge([
    'Currency' => App\Facades\Currency::class,
])->toArray(),

Amb l'alias, pots escriure simplement \Currency::convert(100, 'USD', 'EUR') sense cap use. Això és convenient però fa que les dependències siguin menys visibles al codi. La majoria de projectes moderns prefereixen usar l'import complet en lloc d'aliases globals.

Testing amb Facades#

Una de les raons per les quals les Facades de Laravel són tan populars, malgrat les crítiques sobre les crides estàtiques, és la seva excel·lent testabilitat. Laravel proporciona diversos mecanismes per simular Facades durant els tests, cadascun adequat per a situacions diferents.

Facade::fake()#

El mètode fake() és el mecanisme més potent i el que hauríes d'usar sempre que existeixi. Les Facades que tenen un mètode fake() proporcionen un objecte fals especialitzat que intercepta totes les crides i ofereix mètodes d'asserció específics. Les Facades amb fake() incorporat són: Mail, Queue, Notification, Storage, Event, Bus i Http.

use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use App\Mail\WelcomeMail;
use App\Jobs\ProcessPayment;
use App\Models\User;
use App\Notifications\OrderConfirmed;
 
class RegistrationTest extends TestCase
{
    public function test_registration_sends_welcome_email(): void
    {
        Mail::fake();
 
        $this->postJson('/register', [
            'name' => 'Joan',
            'email' => 'joan@example.com',
            'password' => 'password123',
        ]);
 
        // Verificar que s'ha enviat el correu
        Mail::assertSent(WelcomeMail::class, function ($mail) {
            return $mail->hasTo('joan@example.com');
        });
 
        // Verificar que no s'ha enviat cap altre correu
        Mail::assertSent(WelcomeMail::class, 1);
    }
 
    public function test_order_dispatches_payment_job(): void
    {
        Queue::fake();
 
        $this->postJson('/orders', ['product_id' => 1, 'quantity' => 2]);
 
        Queue::assertPushed(ProcessPayment::class);
        Queue::assertPushed(ProcessPayment::class, function ($job) {
            return $job->order->total === 59.98;
        });
    }
 
    public function test_order_sends_notification(): void
    {
        Notification::fake();
 
        $user = User::factory()->create();
        $this->actingAs($user)->postJson('/orders', [...]);
 
        Notification::assertSentTo($user, OrderConfirmed::class);
    }
 
    public function test_avatar_is_stored(): void
    {
        Storage::fake('avatars');
 
        $file = UploadedFile::fake()->image('avatar.jpg');
 
        $this->postJson('/profile/avatar', ['avatar' => $file]);
 
        Storage::disk('avatars')->assertExists('avatars/' . $file->hashName());
    }
 
    public function test_external_api_call(): void
    {
        Http::fake([
            'api.exchangerate.host/*' => Http::response([
                'result' => 0.85,
            ]),
        ]);
 
        $rate = Currency::getRate('USD', 'EUR');
 
        $this->assertEquals(0.85, $rate);
 
        Http::assertSent(function ($request) {
            return str_contains($request->url(), 'api.exchangerate.host');
        });
    }
}

El fake() és elegant perquè substitueix el servei real per un doble de test que recorda totes les crides i permet verificar-les amb assercions específiques. No s'envia cap correu real, no es processa cap job, no es guarda cap fitxer. Tot queda interceptat.

Facade::shouldReceive()#

Per a Facades que no tenen fake() incorporat (com Cache, DB, Log), pots usar shouldReceive(), que crea un mock basat en Mockery. Això permet definir expectatives sobre quins mètodes es cridaran, amb quins paràmetres i quants cops:

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
 
class DashboardTest extends TestCase
{
    public function test_dashboard_caches_stats(): void
    {
        Cache::shouldReceive('remember')
            ->once()
            ->with('dashboard.stats', 3600, \Mockery::type('closure'))
            ->andReturn((object) ['total' => 42, 'revenue' => 9999.99]);
 
        $response = $this->get('/dashboard');
 
        $response->assertStatus(200);
        $response->assertViewHas('stats');
    }
 
    public function test_dashboard_access_is_logged(): void
    {
        // Permetre qualsevol crida a Cache
        Cache::shouldReceive('remember')->andReturn((object) ['total' => 0, 'revenue' => 0]);
 
        Log::shouldReceive('info')
            ->once()
            ->with('Dashboard accedit', \Mockery::hasKey('user'));
 
        $this->actingAs(User::factory()->create())
            ->get('/dashboard');
    }
}

shouldReceive() utilitza Mockery per darrere, cosa que significa que pots usar tota la potència de Mockery per definir expectatives: once(), twice(), times(3), with(), withArgs(), andReturn(), andThrow(), etc.

Facade::spy()#

Un spy és similar a un mock però amb una diferència important: un mock llança un error si rep una crida que no s'ha definit, mentre que un spy accepta totes les crides i les enregistra perquè puguis verificar-les després. Els spies són útils quan no vols definir expectatives prèvies sinó verificar el comportament a posteriori:

use Illuminate\Support\Facades\Log;
 
public function test_error_is_logged_on_failure(): void
{
    Log::spy();
 
    // Executar l'acció que hauria de provocar un log d'error
    $this->postJson('/process', ['invalid' => 'data']);
 
    // Verificar després que s'ha registrat l'error
    Log::shouldHaveReceived('error')
        ->once()
        ->with(\Mockery::type('string'), \Mockery::type('array'));
}

La diferència entre shouldReceive() i spy() és el moment de la verificació. Amb shouldReceive(), defineixes l'expectativa abans de l'acció (el mock verificarà que es compleixi). Amb spy(), executes l'acció primer i verifiques el comportament després. En la pràctica, els spies produeixen tests més llegibles perquè segueixen el patró Arrange-Act-Assert.

Facade::partialMock()#

Un mock parcial substitueix només alguns mètodes del servei mentre deixa la resta funcionant normalment. Això és útil quan vols interceptar una crida concreta però necessites que la resta de la funcionalitat del servei continuï operant:

use Illuminate\Support\Facades\Cache;
 
public function test_expensive_calculation_is_cached(): void
{
    Cache::partialMock()
        ->shouldReceive('get')
        ->with('expensive.result')
        ->andReturn(42);
 
    // La resta de mètodes de Cache funcionen normalment
    Cache::put('other.key', 'value', 60);
    $this->assertEquals('value', Cache::get('other.key'));
 
    // Però 'expensive.result' retorna el valor simulat
    $this->assertEquals(42, Cache::get('expensive.result'));
}

Com funciona fake() internament#

Quan crides Mail::fake(), Laravel substitueix el binding del servei de mail al contenidor per un objecte MailFake que implementa la mateixa interfície. Totes les crides posteriors a Mail::send() o Mail::to() van a parar al fake en lloc del servei real. Per això fake() ha d'anar al principi del test, abans de qualsevol acció: si envies un correu abans del fake, el correu real s'haurà enviat.

// Això és conceptualment el que fa Mail::fake()
app()->instance('mailer', new MailFake);
 
// Ara qualsevol crida a Mail resol el fake
Mail::to('joan@example.com')->send(new WelcomeMail); // No s'envia res

Recorda que fake() ha de ser la primera instrucció del test. Si fas una acció que envia un correu abans de cridar Mail::fake(), el correu s'haurà enviat de debò. El fake només intercepta les crides que es fan després d'activar-lo.

El debat sobre les Facades#

La comunitat de Laravel té opinions dividides sobre les Facades. Per una banda, els defensors argumenten que simplifiquen el codi, redueixen el boilerplate i fan Laravel accessible a principiants. Per l'altra, els crítics sostenen que les Facades amaguen dependències, compliquen l'anàlisi estàtica del codi i violen principis de disseny orientat a objectes.

La realitat és que les Facades de Laravel no són les crides estàtiques problemàtiques del passat. Una crida estàtica tradicional com Cache::get() en un framework sense contenidor seria realment estàtica: impossible de substituir, impossible de testejar, impossible de canviar la implementació. Les Facades de Laravel, en canvi, són un sucre sintàctic sobre el contenidor de dependències: cada crida es pot substituir, testejar i configurar.

Dit això, hi ha situacions on les Facades poden portar problemes. Si una classe utilitza 10 Facades diferents, això probablement indica que la classe té massa responsabilitats. Amb injecció de dependències, el constructor llarg amb 10 paràmetres seria una senyal d'alarma visual immediata. Amb Facades, les dependències queden repartides pel codi i el problema és menys evident.

En definitiva, les Facades són una eina. Com qualsevol eina, funcionen bé quan s'utilitzen adequadament i malament quan se n'abusa. El que importa no és si uses Facades o injecció de dependències, sinó que el teu codi sigui clar, testable i fàcil de mantenir.

Bones pràctiques#

Per treure el màxim profit de les Facades i evitar-ne els inconvenients, segueix aquestes recomanacions:

Sigues consistent dins del projecte. Si l'equip decideix usar Facades per als serveis del framework i injecció de dependències per als serveis de negoci, mantén aquesta convenció a tot el projecte. La pitjor situació és trobar Cache::get() en un controlador, $this->cache->get() en un altre i cache('key') en un tercer, tots fent el mateix.

No barregis Facades i helpers sense motiu. Tria una convenció i mantén-la. Per exemple, Facades en controladors i serveis, helpers en vistes Blade. Tenir regles clares evita confusió.

Usa injecció de dependències per a serveis personalitzats. Per a serveis del framework (cache, logs, sessions), les Facades són perfectament acceptables perquè tothom les reconeix. Per a serveis de negoci (PaymentGateway, ReportGenerator), la injecció de dependències fa el codi més explícit i fàcil d'entendre.

Documenta les Facades personalitzades amb PHPDoc. Com que l'IDE no pot descobrir automàticament els mètodes disponibles darrere d'una Facade, els blocs @method al PHPDoc són essencials per a una bona experiència de desenvolupament.

Vigila el nombre de Facades per classe. Si un controlador utilitza 5 o més Facades, probablement necessita refactorització. Extreu lògica a serveis dedicats que puguin ser injectats.

Testa amb fake() sempre que sigui possible. Els fakes integrats de Laravel (Mail, Queue, Notification, Storage, Event, Bus, Http) són molt més expressius i fàcils d'usar que els mocks de Mockery. Utilitza shouldReceive() només per a Facades que no tenen fake() incorporat.