Фасады: магия и реальность
Что такое Facade
Facade в Laravel — статический интерфейс к сервису в контейнере.
use Illuminate\Support\Facades\Cache;
Cache::put('key', 'value', 3600);
$value = Cache::get('key');
Под капотом — Cache::put() → app('cache')->put().
Магия
Как это работает:
Cache— подклассIlluminate\Support\Facades\Facade.__callStatic— магический метод.- Через Container резолвится сервис.
- Вызывается метод на сервисе.
Файл 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.