PHP 代理模式

概述

代理模式(Proxy Pattern)是一种结构型设计模式,其核心思想是为目标对象提供一个代理对象,通过代理对象控制对目标对象的访问。代理模式在不改变目标对象接口和核心逻辑的前提下,可在访问前后添加额外操作(如权限校验、日志记录、缓存处理、延迟加载),实现对目标对象的增强和控制。

该模式遵循“单一职责原则”,将目标对象的核心业务逻辑与附加功能(如监控、控制)分离,代理对象专注于访问控制,目标对象专注于业务实现,同时支持动态切换代理类型,提升系统的灵活性和可维护性。

核心价值

核心要点 详细说明
访问控制与增强 代理对象可在调用目标对象前后添加额外逻辑(如权限校验、日志记录、缓存拦截),无需修改目标对象代码
延迟加载优化 对于创建成本高的目标对象(如大文件、数据库连接、远程服务),可通过代理实现懒加载,仅在真正需要时创建目标对象
解耦核心与附加逻辑 目标对象仅关注核心业务,附加功能(如监控、统计)封装在代理中,职责边界清晰,便于维护和扩展
保护目标对象 代理可屏蔽目标对象的直接访问,防止外部直接操作目标对象导致的风险(如敏感数据泄露、非法调用)
灵活扩展 可通过新增代理类扩展不同的附加功能(如日志代理、缓存代理、权限代理),无需修改目标对象和客户端代码

适用场景

场景类型 具体说明
延迟加载场景 如大文件读取、远程服务调用、数据库连接创建等,目标对象创建耗时或消耗资源大,需按需加载
访问控制场景 如接口权限校验、用户身份验证、敏感操作拦截等,需在调用目标对象前验证访问合法性
附加功能增强场景 如操作日志记录、性能监控统计、数据缓存处理、事务管理等,需在不修改目标对象的前提下添加附加功能
远程服务代理场景 如调用第三方API、分布式服务通信等,代理可封装网络请求、参数序列化、异常处理等细节,客户端透明调用
保护目标对象场景 如核心业务组件、敏感数据操作类等,需防止外部直接访问目标对象,通过代理控制访问方式

注意事项

注意点 具体说明
确保接口一致性 代理类与目标对象必须实现同一接口,否则客户端无法透明使用代理对象,破坏模式核心特性
避免过度代理 若目标对象简单且无需附加功能,直接调用目标对象更高效,无需引入代理模式增加系统复杂度
控制代理层级 避免多层代理嵌套(如日志代理→缓存代理→权限代理),否则会增加调用链路长度,影响系统性能
不篡改核心逻辑 代理仅负责访问控制和附加功能,不得修改目标对象的核心业务逻辑和返回结果,确保业务一致性
区分代理类型 根据场景选择合适的代理类型(如静态代理、动态代理),静态代理适用于简单场景,动态代理(如PHP的__call魔术方法、反射)适用于复杂场景

完整代码

以“商品数据查询”为业务场景,目标对象为数据库查询服务,通过代理实现“缓存代理”(查询结果缓存)和“日志代理”(操作日志记录),控制对目标对象的访问并增强功能。

<?php
declare(strict_types = 1);

/**
 * 目标接口:定义商品查询的统一规范
 */
interface ProductServiceInterface
{
    /**
     * 根据商品ID查询商品信息
     * @param int $productId 商品ID
     * @return array 商品信息
     */
    public function getProductById(int $productId): array;

    /**
     * 根据商品分类查询商品列表
     * @param string $category 商品分类
     * @return array 商品列表
     */
    public function getProductsByCategory(string $category): array;
}

/**
 * 目标对象:商品数据库查询服务(核心业务逻辑)
 */
class ProductDatabaseService implements ProductServiceInterface
{
    /**
     * 模拟数据库查询,创建成本高(如建立数据库连接、复杂查询)
     */
    public function getProductById(int $productId): array
    {
        // 模拟数据库查询耗时操作
        sleep(1);
        echo "【数据库查询】查询商品ID:{$productId}的信息" . PHP_EOL;
        return [
            'id' => $productId,
            'name' => "商品{$productId}(数据库查询)",
            'price' => rand(100, 1000),
            'category' => '电子设备'
        ];
    }

    public function getProductsByCategory(string $category): array
    {
        // 模拟数据库查询耗时操作
        sleep(1);
        echo "【数据库查询】查询分类:{$category}的商品列表" . PHP_EOL;
        return [
            ['id' => 101, 'name' => "商品101({$category})", 'price' => 599],
            ['id' => 102, 'name' => "商品102({$category})", 'price' => 799],
            ['id' => 103, 'name' => "商品103({$category})", 'price' => 999]
        ];
    }
}

/**
 * 代理类1:缓存代理(附加缓存功能,减少数据库查询)
 */
class ProductCacheProxy implements ProductServiceInterface
{
    /**
     * 持有目标对象引用
     * @var ProductServiceInterface
     */
    private ProductServiceInterface $target;

    /**
     * 缓存存储(内存缓存,实际可使用Redis等)
     * @var array
     */
    private array $cache = [];

    /**
     * 缓存过期时间(秒)
     * @var int
     */
    private int $cacheExpire = 300;

    public function __construct(ProductServiceInterface $target)
    {
        $this->target = $target;
    }

    public function getProductById(int $productId): array
    {
        $cacheKey = "product:id:{$productId}";
        // 1. 先查询缓存
        if (isset($this->cache[$cacheKey]) && time() - $this->cache[$cacheKey]['time'] < $this->cacheExpire) {
            echo "【缓存命中】从缓存中获取商品ID:{$productId}的信息" . PHP_EOL;
            return $this->cache[$cacheKey]['data'];
        }

        // 2. 缓存未命中,调用目标对象查询
        $data = $this->target->getProductById($productId);

        // 3. 存入缓存
        $this->cache[$cacheKey] = [
            'data' => $data,
            'time' => time()
        ];
        echo "【缓存存储】将商品ID:{$productId}的信息存入缓存" . PHP_EOL;
        return $data;
    }

    public function getProductsByCategory(string $category): array
    {
        $cacheKey = "product:category:{$category}";
        // 1. 先查询缓存
        if (isset($this->cache[$cacheKey]) && time() - $this->cache[$cacheKey]['time'] < $this->cacheExpire) {
            echo "【缓存命中】从缓存中获取分类:{$category}的商品列表" . PHP_EOL;
            return $this->cache[$cacheKey]['data'];
        }

        // 2. 缓存未命中,调用目标对象查询
        $data = $this->target->getProductsByCategory($category);

        // 3. 存入缓存
        $this->cache[$cacheKey] = [
            'data' => $data,
            'time' => time()
        ];
        echo "【缓存存储】将分类:{$category}的商品列表存入缓存" . PHP_EOL;
        return $data;
    }
}

/**
 * 代理类2:日志代理(附加日志记录功能,监控操作)
 */
class ProductLogProxy implements ProductServiceInterface
{
    /**
     * 持有目标对象引用(可嵌套其他代理)
     * @var ProductServiceInterface
     */
    private ProductServiceInterface $target;

    public function __construct(ProductServiceInterface $target)
    {
        $this->target = $target;
    }

    public function getProductById(int $productId): array
    {
        // 1. 调用前记录日志(请求信息)
        $this->log("开始查询商品ID:{$productId}的信息", 'info');

        try {
            // 2. 调用目标对象(或嵌套的其他代理)
            $data = $this->target->getProductById($productId);

            // 3. 调用后记录日志(响应信息)
            $this->log("商品ID:{$productId}查询成功,结果:" . json_encode($data, JSON_UNESCAPED_UNICODE), 'info');
            return $data;
        } catch (\Exception $e) {
            // 4. 异常时记录错误日志
            $this->log("商品ID:{$productId}查询失败,错误:" . $e->getMessage(), 'error');
            throw $e;
        }
    }

    public function getProductsByCategory(string $category): array
    {
        // 1. 调用前记录日志
        $this->log("开始查询分类:{$category}的商品列表", 'info');

        try {
            // 2. 调用目标对象
            $data = $this->target->getProductsByCategory($category);

            // 3. 调用后记录日志
            $this->log("分类:{$category}查询成功,商品数量:" . count($data), 'info');
            return $data;
        } catch (\Exception $e) {
            // 4. 异常日志
            $this->log("分类:{$category}查询失败,错误:" . $e->getMessage(), 'error');
            throw $e;
        }
    }

    /**
     * 模拟日志记录(实际可写入文件、数据库或日志系统)
     * @param string $message 日志内容
     * @param string $level 日志级别
     */
    private function log(string $message, string $level = 'info'): void
    {
        $time = date('Y-m-d H:i:s');
        echo "【{$time}】【{$level}】{$message}" . PHP_EOL;
    }
}

// 测试代码
try {
    echo "=== 测试1:直接调用目标对象(无代理) ===" . PHP_EOL;
    $productService = new ProductDatabaseService();
    $productService->getProductById(1001); // 耗时1秒,无缓存和日志
    echo PHP_EOL;

    echo "=== 测试2:缓存代理 + 日志代理(嵌套代理) ===" . PHP_EOL;
    // 嵌套代理:日志代理包裹缓存代理,缓存代理包裹目标对象
    $target = new ProductDatabaseService();
    $cacheProxy = new ProductCacheProxy($target);
    $logProxy = new ProductLogProxy($cacheProxy);

    // 第一次查询:缓存未命中,走数据库查询,记录日志并缓存
    $logProxy->getProductById(1001);
    echo PHP_EOL;

    // 第二次查询:缓存命中,直接从缓存获取,记录日志
    $logProxy->getProductById(1001);
    echo PHP_EOL;

    // 测试分类查询
    $logProxy->getProductsByCategory('电子设备');
} catch (\Exception $e) {
    echo "错误:" . $e->getMessage();
}

核心结构拆解

代理模式的核心由“目标接口”“目标对象”和“代理对象”三部分组成,结构清晰且职责明确:

结构组成 详细说明
目标接口(ProductServiceInterface) 定义目标对象和代理对象的统一行为规范(如 getProductById getProductsByCategory),确保客户端可透明调用
目标对象(ProductDatabaseService) 实现目标接口,封装核心业务逻辑(如数据库查询),是代理对象的代理目标,不包含任何附加功能
代理对象(ProductCacheProxy、ProductLogProxy 等) 实现目标接口,持有目标对象的引用,在调用目标对象方法前后添加附加逻辑(如缓存、日志),并转发调用给目标对象

关键优化点

优化方向 详细说明
接口统一约束 目标接口严格定义方法签名(参数类型、返回值类型),确保代理对象与目标对象的方法一致性,客户端无需区分调用对象
支持代理嵌套 代理对象可嵌套其他代理(如日志代理包裹缓存代理),实现多种附加功能的组合,且不影响客户端调用逻辑
懒加载优化 缓存代理中实现延迟查询,仅在缓存未命中时调用目标对象,减少不必要的资源消耗(如数据库连接、网络请求)
无侵入增强 目标对象无需任何修改即可被代理增强,附加功能(缓存、日志)封装在代理中,完全遵循“开放-封闭原则”
职责单一明确 每个代理仅负责一项附加功能(缓存代理专注缓存,日志代理专注日志),符合“单一职责原则”,便于维护和扩展

常见错误规避

错误类型 问题描述及解决方案
代理类未实现目标接口 问题:代理类未遵循统一接口,客户端无法透明使用代理对象,导致类型错误或调用异常。
解决:所有代理类必须实现目标接口,确保方法签名与目标对象完全一致
代理直接修改目标对象逻辑 问题:代理类在转发调用时篡改目标对象的参数或返回结果,导致业务逻辑异常。
解决:代理仅添加附加功能,不修改目标对象的输入参数和核心返回结果,确保业务一致性
客户端直接依赖代理实现 问题:客户端代码直接引用具体代理类(如 new ProductCacheProxy()),导致切换代理类型时需修改客户端代码。
解决:客户端应依赖目标接口而非具体代理类,可通过工厂模式创建代理实例,降低耦合
多层代理导致性能损耗 问题:过多代理嵌套(如日志→缓存→权限→事务)导致调用链路过长,系统响应变慢。
解决:合理控制代理层级(建议不超过2层),将关联紧密的附加功能合并为一个代理,减少调用开销
静态代理扩展困难 问题:静态代理需为每个目标接口创建对应代理类,若目标接口方法过多,代理类代码冗余。
解决:复杂场景可使用动态代理(如通过PHP的__call魔术方法、反射机制),自动适配目标接口,减少重复编码

扩展场景示例(新增代理无需修改原有代码)

若业务新增“权限代理”(验证用户是否有权限查询商品),仅需新增代理类,无需修改目标对象、已有代理和客户端代码:

// 新增代理类:权限代理(验证用户权限)
class ProductAuthProxy implements ProductServiceInterface
{
    private ProductServiceInterface $target;
    private array $allowedRoles = ['admin', 'editor', 'viewer']; // 允许访问的角色

    public function __construct(ProductServiceInterface $target)
    {
        $this->target = $target;
    }

    public function getProductById(int $productId): array
    {
        // 1. 权限校验(模拟当前用户角色)
        $currentRole = $this->getCurrentUserRole();
        if (!in_array($currentRole, $this->allowedRoles, true)) {
            throw new \RuntimeException("用户角色【{$currentRole}】无权限查询商品ID:{$productId}");
        }

        // 2. 权限通过,转发调用
        echo "【权限校验通过】用户角色:{$currentRole} 允许查询商品ID:{$productId}" . PHP_EOL;
        return $this->target->getProductById($productId);
    }

    public function getProductsByCategory(string $category): array
    {
        // 1. 权限校验
        $currentRole = $this->getCurrentUserRole();
        if (!in_array($currentRole, $this->allowedRoles, true)) {
            throw new \RuntimeException("用户角色【{$currentRole}】无权限查询分类:{$category}的商品");
        }

        // 2. 转发调用
        echo "【权限校验通过】用户角色:{$currentRole} 允许查询分类:{$category}" . PHP_EOL;
        return $this->target->getProductsByCategory($category);
    }

    /**
     * 模拟获取当前用户角色(实际可从会话、Token中获取)
     * @return string
     */
    private function getCurrentUserRole(): string
    {
        // 模拟不同用户角色,可切换测试
        return 'editor'; // 可选:'admin'/'editor'/'viewer'/'guest'
    }
}

// 测试新增权限代理(嵌套多层代理)
echo "=== 测试3:权限代理 + 日志代理 + 缓存代理 ===" . PHP_EOL;
$target = new ProductDatabaseService();
$cacheProxy = new ProductCacheProxy($target);
$logProxy = new ProductLogProxy($cacheProxy);
$authProxy = new ProductAuthProxy($logProxy);

$authProxy->getProductById(1001); // 权限校验→日志→缓存→数据库(缓存命中)
echo PHP_EOL;

// 测试无权限场景(修改getCurrentUserRole返回'guest')
// $authProxy->getProductsByCategory('电子设备'); // 抛出无权限异常

实际应用场景

场景分类 具体说明
缓存代理应用 如API接口缓存、数据库查询结果缓存、静态资源缓存等,代理拦截重复请求,直接返回缓存结果,提升系统性能
日志代理应用 如接口访问日志、操作审计日志、性能监控日志等,代理记录请求参数、响应结果、耗时等信息,便于问题排查和系统优化
权限代理应用 如后台管理系统接口、敏感数据查询接口等,代理验证用户身份、角色权限,防止非法访问
远程代理应用 如调用第三方支付API、天气接口、地图服务等,代理封装网络请求、参数序列化、异常重试等细节,客户端透明调用
延迟加载应用 如电商平台的商品详情页、大文件下载、视频播放等,代理仅在用户需要时加载目标资源,减少初始加载时间和资源消耗

代理模式与其他模式对比

对比维度 代理模式 适配器模式 装饰器模式
核心目标 控制目标对象访问,添加附加功能 转换接口,解决兼容性问题 动态添加扩展功能,组合多个职责
对象关系 代理持有目标对象引用(代理关系) 适配器组合原始类(适配关系) 装饰器嵌套包装组件(包装关系)
执行逻辑 代理转发调用,添加附加逻辑(不改变核心功能) 适配器转换接口格式,转发调用 多层装饰器依次执行,叠加功能
接口一致性 代理与目标对象实现同一接口 适配器与目标接口一致,与原始类接口不同 装饰器与被装饰对象实现同一接口
适用场景 访问控制、延迟加载、附加功能增强 接口不兼容、第三方组件集成 功能动态扩展、多职责组合
核心差异 侧重“访问控制”,不改变目标对象功能 侧重“接口转换”,解决兼容问题 侧重“功能叠加”,增强目标对象功能