Search by

kode / facade

kodephp

适用于 PHP 8.3+ 的健壮、通用门面组件,兼容 Laravel、Symfony、ThinkPHP、Webman 和 KodePHP。

Package info

github.com/kodephp/facade

pkg:composer/kode/facade

Statistics

Installs: 284

Dependents: 5

Suggesters: 5

Stars: 1

Open Issues: 0

v3.3.0 2026-09-23 01:23 UTC

This package is auto-updated.

Last update: 2026-09-23 01:23:52 UTC


README

包名: kode/facade
版本: 3.3.0 (稳定版)
版本来源: 跟随 Git 标签(如 v3.3.0),composer.json 不再内嵌 version 字段 PHP 版本: >=8.3
作者: KodePHP Team
许可证: Apache-2.0
IDE 支持: PhpStorm, VS Code

📦 概述

kode/facade 是一个健壮、通用、轻量级的 PHP 门面抽象组件,专为 KodePHP 框架设计,同时兼容 Laravel、Symfony、ThinkPHP 8、Webman、自研框架 等主流 PHP 框架。

该组件提供:

  • 静态代理 - 实现服务容器绑定的动态调用
  • PHP 8.3+ 支持 - 使用类型化类常量等 8.3 新特性,支持枚举、只读类等
  • 协程安全 - 完全无副作用,不影响协程、多线程、多进程模型
  • 可调用性缓存 - 用 Closure::fromCallable() 直接调用替代反射 invokeArgs,调用更快
  • 原子实例解析 - 上下文模式下用 Context::getOrSet() 原子解析,解析失败绝不缓存坏状态
  • 上下文隔离 - 支持 Fiber、Swoole、Swow 等协程环境的上下文隔离
  • 跨框架兼容 - 可作为通用组件在任何 PSR-11 容器中使用
  • IDE 友好 - 提供完整的 PHPDoc 智能提示支持

🧩 核心设计理念

特性 说明
🔐 无全局状态污染 不使用 static::$app 全局赋值,通过 ContainerInterface 注入
性能优化 方法调用缓存(可调用闭包)+ 实例身份判定失效,避免重复解析
🔄 协变逆变支持 接口返回类型与参数支持 PHP 泛型风格协变与逆变
🧱 解耦设计 仅依赖 Psr\Container\ContainerInterface,不依赖具体实现
🔧 高度可配置 支持自定义容器实现、门面映射、方法缓存等
🔧 高度可扩展 支持自定义门面映射、方法缓存等

📚 安装方式

composer require kode/facade

🧠 核心类与 API

1. Facade 抽象类(核心)

所有门面的基类,提供静态代理能力。

namespace Kode\Facade;

use Psr\Container\ContainerInterface;

abstract class Facade
{
    /**
     * 获取当前门面对应的服务名(在容器中的 key)
     */
    abstract protected static function id(): string;

    /**
     * 设置服务容器
     */
    public static function setContainer(ContainerInterface $container): void;

    /**
     * 清除当前门面的代理实例(用于测试或重置)
     *
     * 两条缓存(进程级代理 + 当前执行单元)一并作废,并剪掉可调用缓存,
     * 使被替换/被缓存的服务实例可被回收。
     */
    public static function clear(): void;

    /**
     * 清除所有门面的缓存实例
     */
    public static function clearAll(): void;

    /**
     * 检查门面是否已解析
     */
    public static function isResolved(): bool;

    /**
     * 获取此门面实际生效的服务ID(绑定优先于 id())
     */
    public static function getServiceId(): string;

    /**
     * 使用参数数组调用门面实例上的方法
     */
    public static function call(string $method, array $args = []): mixed;

    /**
     * 检查「经门面静态调用」能否走到服务实例上的该方法
     *
     * 基类自身声明的公共方法(clear/hasMethod/call…)与 `__` 前缀魔术名
     * 永远不会被转发,一律返回 false;服务用 __call 兜底的方法返回 true。
     */
    public static function hasMethod(string $method): bool;

    /**
     * 模拟门面实例(用于测试)
     */
    public static function mock(object $mock): void;

    /**
     * 撤销当前门面的模拟
     */
    public static function unmock(): void;

    /**
     * 检查当前门面是否被模拟
     */
    public static function isMocked(): bool;

    /**
     * 绑定当前门面到服务ID(门面自绑定)
     */
    public static function bind(string $serviceId): void;

    /**
     * 解除当前门面的绑定
     */
    public static function unbind(): void;

    /**
     * 运行时热替换当前门面的已解析实例
     */
    public static function swap(object $instance): void;

    /**
     * 启用上下文安全模式
     */
    public static function enableContextSafeMode(): void;

    /**
     * 禁用上下文安全模式
     */
    public static function disableContextSafeMode(): void;

    /**
     * 检查是否启用了上下文安全模式
     */
    public static function isContextSafeMode(): bool;

    /**
     * 动态静态调用转发
     */
    public static function __callStatic(string $method, array $args): mixed;
}

2. FacadeProxy(代理管理器)

管理门面类与实际服务实例之间的映射关系。

namespace Kode\Facade;

final class FacadeProxy
{
    /**
     * 绑定门面到服务ID
     */
    public static function bind(string $facade, string $serviceId): void;

    /**
     * 批量绑定门面
     */
    public static function bindMany(array $bindings): void;

    /**
     * 解除门面绑定
     */
    public static function unbind(string $facade): void;

    /**
     * 检查门面是否已绑定
     */
    public static function isBound(string $facade): bool;

    /**
     * 获取门面对应的服务ID
     */
    public static function getServiceId(string $facade): ?string;

    /**
     * 获取所有门面绑定
     */
    public static function getBindings(): array;

    /**
     * 模拟门面实例(用于测试)
     */
    public static function mock(string $facade, object|Closure $mock): void;

    /**
     * 检查门面是否被模拟
     */
    public static function isMocked(string $facade): bool;

    /**
     * 获取门面实例
     */
    public static function getInstance(string $facade): object;

    /**
     * 清除所有数据
     */
    public static function clearAll(): void;
}

3. ContextualFacadeManager(上下文管理器)

为协程环境提供上下文隔离的门面实例管理。

namespace Kode\Facade;

final class ContextualFacadeManager
{
    /**
     * 设置服务容器(换容器时作废当前执行单元的门面缓存)
     */
    public static function setContainer(ContainerInterface $container): void;

    /**
     * 获取门面实例
     */
    public static function getInstance(string $facadeClass): object;

    /**
     * 检查门面实例是否存在于当前上下文(登记过 mock 同样算已存在)
     */
    public static function hasInstance(string $facadeClass): bool;

    /**
     * 清除当前上下文的所有门面实例
     */
    public static function clearInstances(): void;
}

实现要点(基于 kode/context 3.0 的执行单元隔离 + WeakMap 自动回收):

  • 每个「门面 + 服务ID」在上下文中拥有独立键,而非共享一个可变数组,彻底消除「读-改-写」共享 map 的竞态与类型污染。
  • 实例解析使用 Context::getOrSet() 的原子 get-or-compute 语义:键不存在时才解析并写入;若解析失败(容器/服务异常)异常直接传播,且不会写入任何失败状态,下次调用会重新解析。
  • 批量/单门面清除均按前缀匹配,因此运行时改绑(服务ID 变化)后旧键也能被正确清理。
  • 服务ID 解析复用 FacadeProxy::resolveServiceId()(v3.3.0 起):两条路径共用同一份校验规则, 必须是 Facade 子类且能解析出非空 ID;此前上下文侧只判 class_exists + method_exists, 任意带 getServiceId() 的无关类也能被当门面拿去解析容器服务。

🛠 使用示例

步骤 1:定义一个服务接口

namespace App\Service;

interface MailerInterface
{
    public function send(string $to, string $subject, string $body): bool;
    public function getDriver(): string;
}

步骤 2:实现服务

namespace App\Service;

class SmtpMailer implements MailerInterface
{
    public function send(string $to, string $subject, string $body): bool
    {
        // 发送逻辑...
        return true;
    }

    public function getDriver(): string
    {
        return 'smtp';
    }
}

步骤 3:创建门面

namespace App\Facade;

use Kode\Facade\Facade;

/**
 * 邮件门面
 *
 * @method static bool send(string $to, string $subject, string $body)
 * @method static string getDriver()
 *
 * @see \App\Service\MailerInterface
 */
class Mail extends Facade
{
    protected static function id(): string
    {
        return 'mailer'; // 对应容器中的服务 key
    }
}

步骤 4:在任意框架中使用

Laravel / Symfony / ThinkPHP / Webman 示例

use App\Facade\Mail;
use Kode\Facade\FacadeProxy;

// 绑定门面到服务ID
FacadeProxy::bind(\App\Facade\Mail::class, 'mailer');

// 设置容器
Mail::setContainer($container);

// 使用静态调用
Mail::send('user@example.com', 'Hello', 'Welcome!');
echo Mail::getDriver(); // 输出: smtp

🧩 增强功能

✅ 门面状态检查

// 检查门面是否已解析
if (Mail::isResolved()) {
    // 门面已解析
}

// 检查门面是否绑定到服务ID
if (FacadeProxy::isBound(\App\Facade\Mail::class)) {
    // 门面已绑定
}

✅ 获取门面信息

// 获取门面的服务ID
$serviceId = Mail::getServiceId();

// 获取门面的服务ID(通过代理)
$serviceId = FacadeProxy::getServiceId(\App\Facade\Mail::class);

// 获取所有绑定的门面
$bindings = FacadeProxy::getBindings();

✅ 方法调用增强

// 使用 call 方法调用,参数以数组形式传递
$result = Mail::call('send', ['user@example.com', 'Subject', 'Body']);

// 检查门面实例上是否存在指定方法
if (Mail::hasMethod('send')) {
    // 方法存在
}

✅ 上下文安全模式(Context-Safe Mode)

在协程环境(如 Swoole、Swow、PHP 8.1+ Fiber)中,可以启用上下文安全模式以确保门面实例在不同协程间隔离:

// 启用上下文安全模式
Mail::enableContextSafeMode();

// 现在每个协程将拥有独立的门面实例缓存
// 避免不同协程间的实例污染问题

上下文安全模式特性:

  • ✅ 每个协程/上下文拥有独立的实例缓存
  • ✅ 支持 PHP Fiber、Swoole、Swow 等协程环境
  • ✅ 与原有 API 完全兼容:mock() / swap() / unmock() / clear() 在两条解析路径上语义一致 (v3.2.1 起。此前实例解析只读上下文缓存,开启本模式后 mock() 会静默不生效——测试拿到真服务, 断言却全绿;swap() 同理只写了进程级缓存,本执行单元的下次调用仍看到旧实例)
  • ✅ mock 优先于上下文缓存:unmock() 之后无需清上下文即回退到容器解析
  • setContainer() 换容器即作废当前执行单元的门面缓存(v3.3.0 起与 FacadeProxy 同口径; 此前上下文侧只换引用不清缓存,换完容器仍会返回旧容器创建的对象)
  • isResolved() 在两条路径上口径一致:登记了 mock 即视为已解析(v3.3.0 起)
  • ✅ 可随时启用或禁用
// 检查是否启用了上下文安全模式
if (Mail::isContextSafeMode()) {
    // 上下文安全模式已启用
}

// 禁用上下文安全模式
Mail::disableContextSafeMode();

✅ 测试模拟

// 模拟门面实例
$mockMailer = new class implements \App\Service\MailerInterface {
    public function send(string $to, string $subject, string $body): bool {
        echo "[MOCK] Sending email to {$to}";
        return true;
    }

    public function getDriver(): string {
        return 'mock-driver';
    }
};

Mail::mock($mockMailer);

// 现在调用将使用模拟实例
Mail::send('test@example.com', 'Test', 'Body'); // 输出: [MOCK] Sending email to test@example.com

✅ 门面级绑定与热替换

除了直接操作 FacadeProxy,也可在门面自身上完成绑定、解绑、热替换与模拟检查, 让 Facade 成为完整的业务侧主 API,无需额外引用 FacadeProxy

use App\Facade\Mail;

// 门面自绑定(等价于 FacadeProxy::bind(Mail::class, 'mailer'))
Mail::bind('mailer');

// 运行时热替换实例(不影响 isMocked 判定,clear() 即可回退)
Mail::swap($alternativeMailer);
echo Mail::getDriver(); // 使用替换后的实例

// 检查 / 撤销模拟
if (Mail::isMocked()) {
    Mail::unmock();
}

// 解绑
Mail::unbind();

getServiceId() 返回实际生效的服务ID:显式绑定优先于门面自身 id(), 与实例解析逻辑完全一致。

🧩 高级特性

✅ 协变(Covariance)支持

interface ResponseFactory
{
    public function make(): Response; // 返回基类
}

interface JsonResponseFactory extends ResponseFactory
{
    public function make(): JsonResponse; // 子类返回更具体的类型(协变)
}

kode/facade 完全支持此类返回类型的协变。

✅ 逆变(Contravariance)支持

interface EventDispatcher
{
    public function dispatch(object $event): void;
}

interface SpecificEventDispatcher extends EventDispatcher
{
    public function dispatch(SpecificEvent $event): void; // 参数更具体(逆变)
}

✅ 参数类型的逆变在反射调用中被正确处理。

✅ 反射安全调用(带缓存)

内部用 Closure::fromCallable([$instance, $method]) 直接调用,比反射 invokeArgs 开销更低; 按「门面 + 方法」缓存,并以实例对象身份(!==)判定失效,实例变化(mock / swap / clear)即自动重建:

$callable = Closure::fromCallable([$instance, $method]);
$callable(...$args);

缓存条目持有实例对象的强引用,因此 clear() / clearAll() / setContainer() / mock() / swap() / bind() 都会顺手剪掉该门面的缓存(v3.3.0 起)——否则换过一轮驱动的老服务对象 会被永久钉住,在常驻 worker 里表现为一块降不下来的内存地板。

✅ 转发边界:魔术名与基类同名(v3.3.0)

两道边界都是显式拒绝,不再"能调就调":

final class Mail extends Facade { protected static function id(): string { return 'mailer'; } }

Mail::__construct('admin');        // FacadeException::CODE_MAGIC_METHOD —— 不进服务的构造函数
Mail::call('__clone', []);        // 同上,__destruct / __serialize 等一并拦下

Closure::fromCallable([$instance, '__construct']) 在 PHP 里是合法可调用,放行等于让调用方 绕过容器的初始化约束、带任意参数重跑服务对象构造并改写其内部状态。__ 前缀是 PHP 的保留命名, 门面只转发业务方法。

第二条边界是基类同名遮蔽:PHP 恒定优先调用已声明的方法,__callStatic 只在名字不可访问时才触发, 所以 clear() / call() / hasMethod() 这类基类公共静态名永远走不到服务。这不是 bug(语言规则), 但必须说清:

// 服务 CacheManager 自己也有 clear()
Cache::clear();              // 清的是门面实例缓存,不是缓存池
Cache::hasMethod('clear');   // false —— 如实告知静态调用到不了
Cache::call('clear');        // 这才是缓存池的 clear()

定义门面时请避开基类的公共保留名(id() 是 protected,不在此列):getInstance setContainer clear clearAll mock unmock isMocked bind unbind swap isResolved getServiceId call hasMethod enableContextSafeMode disableContextSafeMode isContextSafeMode resetState __call __callStatic

门面是透明代理:服务方法自身抛出的业务异常原样向上传播,绝不被包装成 FacadeException

🧪 测试与兼容性

框架 兼容性 说明
Laravel 9+ 使用 app()Container 注入
Symfony 6+ 通过 ServiceContainer 传入
ThinkPHP 8 使用 app() 兼容 PSR 容器
Webman 1+ 支持 Workerman 多进程模型
Swoole 协程 无全局变量,协程安全
多线程(ZTS) 不使用静态实例缓存线程局部存储

📂 包结构

vendor/kode/facade/
├── src/
│   ├── Facade.php                 # 门面抽象基类
│   ├── FacadeProxy.php            # 门面代理管理器
│   ├── ContextualFacadeManager.php # 上下文安全门面管理器
│   └── Exception/
│       └── FacadeException.php    # 门面异常类
├── tests/
│   └── Unit/
│       ├── FacadeTest.php         # 门面测试
│       └── ContextualFacadeTest.php # 上下文门面测试
├── composer.json
├── LICENSE
└── README.md

📄 composer.json

{
    "name": "kode/facade",
    "type": "library",
    "description": "适用于 PHP 8.3+ 的健壮、通用门面组件,兼容 Laravel、Symfony、ThinkPHP、Webman 和 KodePHP。",
    "keywords": ["facade", "proxy", "static", "container", "psr", "kodephp"],
    "license": "Apache-2.0",
    "authors": [
        {
            "name": "半本正经",
            "email": "382601296@qq.com"
        }
    ],
    "require": {
        "php": "^8.3",
        "psr/container": "^1.0 || ^2.0",
        "kode/context": "^3.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0"
    },
    "autoload": {
        "psr-4": {
            "Kode\\Facade\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Kode\\Facade\\Tests\\": "tests/"
        }
    }
}

📌 最佳实践建议

  1. 门面类名:使用单数、动词或名词,如 MailCacheLogDB
  2. 方法名:保持与服务接口一致,避免动词重复(如 getGet
  3. 不覆盖 __callStatic:避免破坏代理机制
  4. 绑定在启动时完成:在 bootstrap.phpServiceProvider 中调用 FacadeProxy::bind()
  5. 测试时使用 clear():避免测试间状态污染
  6. 协程环境启用上下文安全模式:确保实例隔离
  7. 别给服务方法起名 clear / call / swap 之类基类同名:PHP 恒优先调用已声明的方法, 这类名字经门面走不到服务;只能靠 Facade::call('name') 显式转发,hasMethod('name') 也会如实报 false

📞 联系与贡献

kode/facade —— 简单、安全、通用、高性能的 PHP 门面解决方案
为未来协程、多线程、多进程架构打下坚实基础。