门面模式
概念说明
门面(Facade)为框架的核心服务提供了 静态代理接口,允许以静态方法的方式调用容器中服务的实例方法。它是连接开发者与框架内部实现的桥梁,兼顾了易用性和灵活性。
Viswoole 的 Facade 通过 PHP 的 __callStatic 魔术方法实现:当调用 Facade 上的静态方法时,它会从容器中获取对应的真实服务实例,然后将调用转发至该实例的同名方法。
工作原理
调用转发机制
text
Facade::methodName($args)
│
▼
__callStatic('methodName', $args)
│
▼
getMappingClass() → 获取容器中的真实服务名称
│
▼
app()->make('服务名') → 从容器解析实例
│
↓
$instance->methodName($args) → 调用真实方法内置门面列表
| 门面类 | 对应服务 | 说明 |
|---|---|---|
Facade\App | App | 应用实例,提供工厂方法、路径解析等 |
Facade\Config | Config | 配置读取与管理 |
Facade\Env | Env | 环境变量读取 |
Facade\Event | Event | 事件系统的注册与触发 |
Facade\Middleware | Middleware | 中间件管理 |
Facade\Server | Server | HTTP Server 管理 |
Facade\Task | Task | 异步任务投递 |
全局辅助函数
除了 Facade 类,框架还提供了全局辅助函数(定义在 helper.php),它们本质上是 Facade 的快捷包装:
| 函数 | 等价调用 | 说明 |
|---|---|---|
app() | App::factory() | 获取应用/容器实例 |
config($key) | Config::get($key) | 获取配置值 |
env($key, $default) | Env::get($key, $default) | 获取环境变量 |
cache(...) | CacheManager 实例或 get | 获取缓存值或缓存管理器实例 |
invoke(...) | Container::invoke() | 调用方法并注入参数 |
bind(...) | Container::bind() | 绑定服务 |
make(...) | Container::make() | 解析实例 |
getVersion() | 获取框架版本号 | - |
代码示例
使用内置门面
php
use Viswoole\Core\Facade\Config;
use Viswoole\Core\Facade\Env;
use Viswoole\Core\Facade\Event;
use Viswoole\Core\Facade\App;
// 读取配置
$timeZone = Config::get('app.default_timezone', 'UTC');
$isDebug = Config::get('app.debug', false);
// 读取环境变量
$dbHost = Env::get('DB_HOST', 'localhost');
$appKey = Env::get('APP_KEY');
// 事件操作
Event::on('user.login', function ($userId) { /* ... */ });
Event::emit('user.login', [1]);
// 应用信息
$rootPath = App::getRootPath();
$version = App::getVersion();使用全局辅助函数
php
// 辅助函数无需 use 导入,在任何地方可直接使用
// 获取容器实例
$app = app();
// 配置与环境
$timezone = config('app.default_timezone');
$debug = env('APP_DEBUG', false);
// 容器操作
$logger = make(Logger::class);
bind(CacheInterface::class, RedisCache::class);
// 带参数注入的方法调用
$result = invoke([new UserService(), 'find'], ['id' => 1]);自定义门面
为自有服务创建 Facade,只需继承基类并指定服务名称:
php
use Viswoole\Core\Facade;
class Payment extends Facade
{
/**
* 获取门面对应的容器服务标识
*/
protected static function getMappingClass(): string
{
return PaymentService::class;
}
}
// 使用自定义门面
Payment::pay(['amount' => 99.00, 'order_no' => 'ORD001']);
Payment::refund('ORD001');门面 vs 直接注入
php
// 方式一:使用门面(适合控制器、路由闭包等场景)
class OrderController
{
public function create()
{
// 直接通过门面调用,简洁明了
$result = Payment::process($this->request->all());
return json($result);
}
}
// 方式二:构造函数注入(适合 Service 层等需要测试的场景)
class OrderService
{
public function __construct(
private PaymentServiceInterface $payment,
) {}
public function createOrder(array $data): array
{
// 通过注入的依赖调用,便于 mock 测试
return $this->payment->process($data);
}
}最佳实践
1. 控制器与路由中使用门面
在控制器和路由闭包中,门面是最便捷的选择:
php
// ✅ 推荐:在控制器中使用门面
class UserController extends BaseController
{
public function show(int $id): ResponseInterface
{
$user = Db::table('users')->find($id);
Cache::put("user:{$id}", $user, 3600);
return json($user);
}
}
// ✅ 推荐:路由闭包中使用辅助函数
Route::get('/api/info', function () {
return [
'version' => getVersion(),
'debug' => config('app.debug'),
];
});2. 服务层使用依赖注入
在 Service 等核心业务层,优先使用构造函数注入以保证可测试性:
php
// ✅ 推荐:Service 层通过构造函数声明依赖
class OrderService
{
public function __construct(
private PaymentServiceInterface $payment,
private InventoryServiceInterface $inventory,
) {}
public function placeOrder(array $data): Order
{
$this->inventory->reserve($data['items']);
return $this->payment->charge($data);
}
}
// 便于编写单元测试时 mock 依赖
$service = new OrderService(
payment: new MockPaymentService(),
inventory: new MockInventoryService(),
);3. 不要滥用门面
门面虽然方便,但会增加隐式依赖,降低代码的可追踪性:
php
// ❌ 不推荐:在一个方法中混用大量门面,隐式依赖过多
class ReportService
{
public function generate()
{
$users = Db::table('users')->get(); // 隐式依赖 DB
$orders = Db::table('orders')->get(); // 隐式依赖 DB
Cache::put('report', $data, 3600); // 隐式依赖 Cache
Event::emit('report.generated', [$data]); // 隐式依赖 Event
Log::info('报告生成完成'); // 隐式依赖 Log
}
}
// ✅ 推荐:显式声明核心依赖,辅助性操作可用门面
class ReportService
{
public function __construct(
private DatabaseManager $db,
) {}
public function generate()
{
$users = $this->db->table('users')->get();
$orders = $this->db->table('orders')->get();
Cache::put('report', $data, 3600); // 辅助操作,可用门面
Event::emit('report.generated', [$data]); // 辅助操作,可用门面
}
}4. 自定义门面的命名规范
自定义门面类命名应与其代理的服务对应,放在 App\Facade 命名空间下:
php
namespace App\Facade;
use Viswoole\Core\Facade;
use App\Service\NotificationService;
/**
* 通知服务门面
* 提供 NotificationService 的静态代理访问
*/
class Notification extends Facade
{
protected static function getMappingClass(): string
{
return NotificationService::class;
}
}