Урок 03

Фасады: магия и реальность

Что такое Facade

Facade в Laravel — статический интерфейс к сервису в контейнере.

use Illuminate\Support\Facades\Cache;

Cache::put('key', 'value', 3600);
$value = Cache::get('key');

Под капотом — Cache::put() → app('cache')->put().

Магия

Как это работает:

  1. Cache — подкласс Illuminate\Support\Facades\Facade.
  2. __callStatic — магический метод.
  3. Через Container резолвится сервис.
  4. Вызывается метод на сервисе.

Файл Illuminate\Support\Facades\Cache.php:

class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}

'cache' — ключ в контейнере.

Как устроен Facade

Базовый класс:

abstract class Facade
{
    protected static function getFacadeAccessor(): string
    {
        throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
    }

    public static function __callStatic(string $method, array $args): mixed
    {
        $instance = static::getFacadeRoot();

        return $instance->$method(...$args);
    }

    public static function getFacadeRoot(): mixed
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }
}

Ключевой метод — __callStatic.

Создание своего Facade

1. Сервис:

namespace App\Services;

class Calculator
{
    public function sum(int ...$nums): int
    {
        return array_sum($nums);
    }
}

2. Facade:

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static int sum(int ...$nums)
 */
class Calculator extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \App\Services\Calculator::class;
    }
}

3. Использование:

use App\Facades\Calculator;

Calculator::sum(1, 2, 3);   // 6

Контейнер сам создаст Calculator.

PHPDoc для IDE

IDE не знает о магических вызовах.

Аннотация:

/**
 * @method static mixed get(string $key, mixed $default = null)
 * @method static void put(string $key, mixed $value, int $seconds = null)
 * @method static bool forget(string $key)
 *
 * @see \Illuminate\Contracts\Cache\Repository
 */
class Cache extends Facade { }

Laravel 8+ — @method для каждого метода.

Встроенные Facades

Основные:

  • Cache — кеш.
  • DB — база.
  • Log — логи.
  • Request — запрос.
  • Response — ответ.
  • Route — маршруты.
  • Auth — аутентификация.
  • Gate — авторизация.
  • Mail — почта.
  • Storage — файлы.
  • URL — URLs.
  • Validator — валидация.
  • View — шаблоны.
  • Config — конфиг.
  • Artisan — CLI.
  • App — приложение.
  • Session — сессии.

Полный список — vendor/laravel/framework/src/Illuminate/Support/Facades/.

Facade vs DI

Facade:

use Illuminate\Support\Facades\Cache;

class UserService
{
    public function getUser(int $id): User
    {
        return Cache::remember("user:$id", 3600, fn() => User::find($id));
    }
}

DI:

use Illuminate\Contracts\Cache\Repository;

class UserService
{
    public function __construct(
        private Repository $cache,
    ) {}

    public function getUser(int $id): User
    {
        return $this->cache->remember("user:$id", 3600, fn() => User::find($id));
    }
}

Что лучше?

DI:

  • Явные зависимости.
  • Легче тестировать.
  • Правильно с точки зрения SOLID.

Facade:

  • Короче.
  • Удобно в контроллерах.
  • Меньше boilerplate.

Правило Laravel:

  • Контроллеры / короткие классы — Facade.
  • Сервисы / Use Cases — DI.

Тестирование Facades

Facade::fake():

use Illuminate\Support\Facades\Cache;

public function test_users_cached(): void
{
    Cache::shouldReceive('get')
        ->once()
        ->with('user:1')
        ->andReturn(null);

    Cache::shouldReceive('put')
        ->once();

    $service = new UserService();
    $service->getUser(1);
}

Мок работает как обычный.

Или через shouldReceive:

Cache::shouldReceive('remember')
    ->andReturnUsing(fn($key, $ttl, $cb) => $cb());

Facade и Unit тесты

⚠️ С shouldReceive — Facade становится глобальным моком.

Проблема: тесты могут влиять друг на друга.

Правило: мокай только то, что используешь.

Реальный пример

Контроллер с Facade:

class UserController extends Controller
{
    public function index(): View
    {
        $users = Cache::remember('users', 3600, fn() => User::all());

        Log::info('Users listed', ['count' => $users->count()]);

        return view('users.index', compact('users'));
    }
}

Контроллер с DI:

class UserController extends Controller
{
    public function __construct(
        private Repository $cache,
        private LoggerInterface $logger,
    ) {}

    public function index(): View
    {
        $users = $this->cache->remember('users', 3600, fn() => User::all());

        $this->logger->info('Users listed', ['count' => $users->count()]);

        return view('users.index', compact('users'));
    }
}

Первый — короче, второй — правильнее с точки зрения ООП.

Facade — не паттерн GoF

⚠️ Facade в Laravel ≠ Facade из GoF.

GoF Facade — упрощение сложной системы.

Laravel Facade — статический прокси.

Скрытая связанность

Проблема:

class OrderService
{
    public function create(): void
    {
        Cache::put('last_order', time(), 3600);   // скрытая зависимость
        Log::info('Order created');
    }
}

Не видно зависимостей в конструкторе.

Решение: DI в сервисах.

Практика: свой Facade

<?php

// ===== Service =====
namespace App\Services;

class SmsSender
{
    public function __construct(
        private string $apiKey,
    ) {}

    public function send(string $phone, string $message): bool
    {
        // отправка
        return true;
    }

    public function balance(): int
    {
        return 100;
    }
}

// ===== Facade =====
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static bool send(string $phone, string $message)
 * @method static int balance()
 *
 * @see \App\Services\SmsSender
 */
class Sms extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \App\Services\SmsSender::class;
    }
}

// ===== Provider =====
namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class SmsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(\App\Services\SmsSender::class, function ($app) {
            return new \App\Services\SmsSender(
                $app['config']['services.sms.key'],
            );
        });
    }
}

// ===== Usage =====
use App\Facades\Sms;

Sms::send('+79001234567', 'Привет');
$balance = Sms::balance();

Итог

  • Facade — статический прокси к сервису.
  • getFacadeAccessor() — ключ в контейнере.
  • __callStatic — магия.
  • DI vs Facade — оба подхода.
  • Facade — короче, DI — правильнее.
  • shouldReceive — для тестов.
  • Facade Laravel ≠ Facade GoF.

Дальше — Middleware: паттерн Chain of Responsibility.