ThinkPHP 中闭包在数组查询条件中的深度应用

 

一、闭包与数组条件的协同原理

ThinkPHP 的查询体系中,数组条件是构建查询逻辑的核心载体。当数组条件的值为闭包(Closure)时,框架会自动将其解析为动态子查询生成器,实现运行时按需构建 SQL 片段的能力。这种特性源于闭包的词法作用域捕获机制—— 闭包能够记住定义时的外部变量环境,并在执行时动态生成对应的查询逻辑。

核心执行机制

  1. 闭包初始化:通过use关键字捕获外部变量(如用户 ID、请求参数)。
  2. 子查询构建:闭包内部通过$query对象调用查询方法(where/field/join等),定义子查询逻辑。
  3. 主查询整合:框架将闭包生成的子查询结果注入主查询条件(如IN/NOT IN/EXISTS),完成 SQL 拼接。

底层实现逻辑

 

// ThinkPHP查询构造器解析闭包的关键逻辑

if ($conditionValue instanceof \Closure) {

    $closure = $conditionValue;

    $closure($this->query); // 执行闭包生成子查询

    $subQuery = $this->query->buildSql(); // 获取子查询SQL

    // 按条件类型(如NOT IN)整合到主查询

}

二、实战案例:基于闭包的复杂条件过滤

案例背景:未被举报的用户筛选

需求:查询未被当前用户($user_id)举报的文章点赞记录,条件为:

  • 点赞用户 IDlike_article.user_id)不在举报表(like_community_report)的被举报用户 IDto_user_id)中。
  • 举报类型为 2(文章举报)。

完整实现代码

 

use think\facade\Db;

// 1. 定义闭包条件

$user_id = 123; // 当前用户ID

$map = []; // 初始化条件数组

$map[] = [

    'like_article.user_id', // 主查询字段

    'not in', // 条件操作符

    function ($query) use ($user_id) { // 闭包子查询

        $query->name('like_community_report') // 指定子查询表

            ->where([ // 子查询条件

                'type' => 2, // 举报类型为文章

                'user_id' => $user_id // 当前用户发起的举报

            ])

            ->field('to_user_id'); // 子查询结果字段

    }

];

// 2. 执行主查询

$result = Db::name('like_article') // 主表:文章点赞记录

    ->where($map) // 应用闭包条件

    ->select(); // 执行查询

生成的 SQL 分析

 

SELECT * FROM `like_article`

WHERE `like_article`.`user_id` NOT IN (

    SELECT `to_user_id` FROM `like_community_report`

    WHERE `type` = 2 AND `user_id` = 123

);

关键优势

  • 动态参数安全$user_id由闭包捕获并自动转义,避免 SQL 注入。
  • 逻辑模块化:子查询逻辑封装在闭包内,主查询结构清晰易读。
  • 延迟执行优化:子查询仅在主查询执行时生成,减少预查询开销。

三、闭包条件的高级应用模式

1. 多闭包组合查询(AND 条件)

场景:筛选既未被举报,也未被收藏的用户。

 

$map = [

    // 条件1:不在举报列表

    [

        'user_id',

        'not in',

        function ($q) use ($user_id) {

            $q->name('report')->where('user_id', $user_id)->field('target_id');

        }

    ],

    // 条件2:不在收藏列表

    [

        'user_id',

        'not in',

        function ($q) use ($user_id) {

            $q->name('favorite')->where('user_id', $user_id)->field('item_id');

        }

    ]

];

$result = Db::name('user')->where($map)->select();

2. 闭包与 OR 条件结合

场景:查询未被举报,或举报类型不为文章的记录。

 

$map = [

    'OR' => [

        [ // 条件A:不在举报列表

            'user_id',

            'not in',

            function ($q) use ($user_id) {

                $q->name('report')->where('user_id', $user_id)->field('target_id');

            }

        ],

        [ // 条件B:举报类型不为2

            'type',

            '<>',

            2

        ]

    ]

];

$result = Db::name('record')->where($map)->select();

3. 闭包内的关联查询

场景:查询未被举报的文章,并关联作者信息。

 

$result = Db::name('article')

    ->alias('a')

    ->join('user u', 'a.author_id = u.id')

    ->where([

        'a.author_id',

        'not in',

        function ($q) use ($user_id) {

            $q->name('report')

                ->where([

                    'type' => 2,

                    'user_id' => $user_id

                ])

                ->field('target_id');

        }

    ])

    ->field('a.title, u.nickname')

    ->select();

四、闭包条件的关键注意事项

1. 变量作用域控制

  • 值传递(推荐):通过use ($var)传递变量值,避免闭包修改外部变量。

 

$page = 1;

$closure = function() use ($page) { // 闭包内使用$page的副本

    echo $page; // 输出1

};

$page = 2;

$closure(); // 仍输出1

  • 引用传递(谨慎使用):通过use (&$var)传递变量引用,闭包内修改会影响外部。

 

$count = 0;

$closure = function() use (&$count) {

    $count++;

};

$closure();

echo $count; // 输出1

2. 循环中的闭包陷阱

反例:闭包捕获循环变量的最后一个值

 

$ids = [1, 2, 3];

$closures = [];

foreach ($ids as $id) {

    $closures[] = function() use ($id) { // 捕获的是循环结束后的$id3

        echo $id;

    };

}

foreach ($closures as $cb) {

    $cb(); // 输出3, 3, 3

}

正例:通过临时变量固定当前值

 

$ids = [1, 2, 3];

$closures = [];

foreach ($ids as $id) {

    $temp = $id; // 创建临时变量

    $closures[] = function() use ($temp) { // 捕获临时变量的值

        echo $temp;

    };

}

foreach ($closures as $cb) {

    $cb(); // 输出1, 2, 3

}

3. 性能优化策略

  • 预定义闭包:在循环外创建闭包,避免重复生成。

 

// 反例:循环内每次创建新闭包

for ($i=0; $i<1000; $i++) {

    $map[] = ['id', '>', function() use ($i) { ... }];

}

// 正例:循环外创建闭包模板

$closureTemplate = function($i) {

    return function ($q) use ($i) {

        $q->where('id', '>', $i);

    };

};

for ($i=0; $i<1000; $i++) {

    $map[] = ['id', '>', $closureTemplate($i)];

}

  • 避免深层嵌套:超过 3 层闭包嵌套可能导致 SQL 可读性下降,可拆分为分步查询。
  • 利用缓存:对重复使用的闭包结果,通过Db::cache()缓存查询结果。

五、与传统查询方式的对比分析

 

维度

闭包条件查询

传统数组 / 字符串查询

动态性

运行时动态生成子查询

需提前拼接条件字符串

安全性

自动参数转义,防 SQL 注入

字符串拼接需手动转义

可读性

逻辑模块化,贴近自然语言

复杂条件易导致数组嵌套混乱

维护成本

闭包可复用,修改集中

条件分散,修改成本高

性能影响

单次查询开销低

多次预查询可能增加内存占用

典型场景对比:传统子查询方式需先获取子查询结果:

 

// 传统方式:先查询被举报用户ID

$reportedIds = Db::name('report')

    ->where('user_id', $user_id)

    ->column('target_id');

// 再构建IN条件

$map[] = ['user_id', 'not in', $reportedIds];

闭包方式直接嵌入子查询逻辑:

 

// 闭包方式:子查询逻辑内联

$map[] = [

    'user_id',

    'not in',

    function ($q) use ($user_id) {

        $q->name('report')->where('user_id', $user_id)->field('target_id');

    }

];

结论:闭包方式减少了中间变量和预查询步骤,尤其适合子查询结果依赖动态参数的场景。

六、最佳实践与扩展方向

1. 代码规范建议

  • 闭包命名:对复杂闭包使用变量命名,提升可读性。

 

$buildReportSubquery = function ($q, $userId) {

    $q->name('report')->where('user_id', $userId)->field('target_id');

};

$map[] = ['user_id', 'not in', $buildReportSubquery];

  • 注释说明:在闭包上方添加注释,说明其业务逻辑。

 

// 筛选未被当前用户举报的目标ID

$map[] = [

    'user_id',

    'not in',

    function ($q) use ($user_id) { /* ... */ }

];

2. 扩展应用场景

  • 权限过滤:在后台管理系统中,通过闭包动态生成权限范围内的查询条件。
  • 多语言支持:根据用户语言设置,通过闭包动态调整查询的国际化字段。
  • 异步任务:在队列任务中传递闭包,实现延迟执行的动态查询(需注意闭包的序列化支持)。
  • 打印生成的 SQL:通过buildSql()方法查看最终执行的 SQL

3. 调试与测试技巧

 

$sql = Db::name('like_article')->where($map)->buildSql();

echo $sql; // 输出完整SQL语句

  • 单元测试闭包:对闭包单独测试,验证子查询结果是否符合预期。

 

public function testClosureSubquery() {

    $query = $this->app->db->query();

    $closure = function ($q) { /* 闭包逻辑 */ };

    $closure($query);

    $this->assertSame('SELECT target_id...', $query->buildSql());

}

七、总结

闭包与数组条件的结合是 ThinkPHP 中实现动态查询的强大工具,其核心价值在于:

  1. 逻辑封装:将复杂子查询逻辑封装为可复用的闭包单元。
  2. 动态适配:根据运行时变量(如用户 ID、请求参数)动态生成查询条件。
  3. 安全高效:避免 SQL 注入风险,减少预查询和中间变量的性能开销。

在实际开发中,建议从简单的IN/NOT IN场景入手,逐步掌握闭包在关联查询、组合条件中的应用。同时,需注意变量作用域控制和性能优化,确保在提升代码灵活性的同时,保持系统的稳定性和执行效率。

 

原创作者: zlf2000 转载于: https://www.cnblogs.com/zlf2000/p/18846174
帮我找出这个接口 查询出了所有人的原因 /** * 联系人列表 *@create_time 2025-11-06 17:45:47 */ public function get_lianiren($data = [], $user = [], $member_id = 0) { $member_id = ltrim('m14666', 'm'); $return_data = []; if (isset($data['hash']) && isset($data['alias']) && isset($data['data'])) { $data = $data['data']; } try { // 设置变量 // 设置查询 $whereOr[] = ['to_user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; // 设置变量收藏列表 // 查询联系人记录IDS $where = []; $where[] = ['user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; $list_rows = 15; $query_3 = Db::table('lb_user_lianxiren_log') ->where($where) ->whereOr($whereOr) ->field("to_user_id,user_id") ->select(); foreach ($query_3 as $key_1761379556731 => $value_1761379556731) { if($value_1761379556731['to_user_id'] == \app\comfunction\service\User::get_token_user_id($user)){ // 扩展对象收藏列表 $lianxiren_arr[] = $value_1761379556731['user_id']; } else{ // 扩展对象收藏列表 $lianxiren_arr[] = $value_1761379556731['to_user_id']; } } // 设置变量收藏列表 if(!empty($lianxiren_arr)){ // 设置变量收藏列表 $lianxiren_arr = array_unique($lianxiren_arr); // 设置查询条件收藏列表 $whereIn[] = ['id', 'in', $lianxiren_arr]; } // 多表查询会员列表&联系人记录&学历管理 $whereUser = []; $whereUser[] = ['user.status', '=', 1]; $whereUser[] = ['user.is_del', '=', 0]; // dump($whereIn);die; $list_rows = 15; $query_16 = Db::table('lb_user') ->alias('user') ->join('lb_user_lianxiren_log userlianxirenlog', 'userlianxirenlog.user_id = user.id', 'inner') ->join('lb_xueli xueli', 'xueli.aid = user.xueli_id', 'left') ->where($whereUser) ->where($whereIn) ->order('userlianxirenlog.aid desc') ->group("user.id") ->field("user.id,user.birthday,user.nickname,user.avatar,user.mobile,user.gender,user.zhaopian,user.shengao,user.tizhong,user.gongzuo_address,user.zhiye,user.hukou_address,user.laojia_address,user.fangchan_status,user.cheliang_status,user.age,user.xueli_id,user.yueshouru_id") ->field("userlianxirenlog.to_user_id,userlianxirenlog.last_time") ->field("xueli.academic_degree_name as xueli_name") ->paginate() ->toArray(); foreach ($query_16['data'] as $key => &$item) { $item['last_time'] = $item['last_time'] == 0 ? 0 : date('Y-m-d H:i:s', $item['last_time']); } if(empty($query_16)){ throw new \Exception(lang('暂无联系人'), 1); } foreach ($query_16['data'] as $key_1760685816720 => &$value_1760685816720) { if($value_1760685816720['user_id'] == \app\comfunction\service\User::get_token_user_id($user)){ // 设置变量收藏列表 $is_me = 1; } else{ // 设置变量收藏列表 $is_me = 0; } // 设置变量 // 设置查询条件 $whereOr[] = ['to_user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; // 查询解锁记录 $where = []; $where[] = ['user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; $query_27 = Db::table('lb_user_jiesuo_log') ->where($where) ->count(); // 设置 默认没收藏 $is_collect = 0; if($value_1760685816720['user_id'] == \app\comfunction\service\User::get_token_user_id($user)){ // 查询我是否收藏过他 $where = []; $where[] = ['shoucang_user_id', '=', $query_16['to_user_id']]; $where[] = ['user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; $query_30 = Db::table('lb_user_shoucang') ->where($where) ->count(); // 设置变量收藏列表 $is_collect = $query_30; } else{ // 继续查询我是否收藏他 $where = []; $where[] = ['shoucang_user_id', '=', $query_16['user_id']]; $where[] = ['user_id', '=', \app\comfunction\service\User::get_token_user_id($user)]; $query_33 = Db::table('lb_user_shoucang') ->where($where) ->count(); // 设置变量收藏列表 $is_collect = $query_33; } // 查询月收入管理 $where = []; $where[] = ['aid', '=', $value_1760685816720['yueshouru_id']]; $query_36 = Db::table('lb_yueshouru') ->where($where) ->value('income_name'); // 扩展对象 $value_1760685816720['age'] = get_age($value_1760685816720['birthday']); $value_1760685816720['gongzuo_address'] = \app\comfunction\service\Address::transferAreaName($value_1760685816720['gongzuo_address']); $value_1760685816720['fangchan_status'] = \app\comfunction\service\Tools::transferProperty($value_1760685816720['fangchan_status'],'user','fangchan_status',14666); $value_1760685816720['cheliang_status'] = \app\comfunction\service\Tools::transferProperty($value_1760685816720['cheliang_status'],'user','cheliang_status',14666); $value_1760685816720['is_jiesuo'] = $query_27; $value_1760685816720['is_collect'] = $is_collect; $value_1760685816720['gender'] = \app\comfunction\service\Tools::transferProperty($value_1760685816720['gender'],'user','gender',14666); $value_1760685816720['yueshouru_name'] = $query_36; $value_1760685816720['is_me'] = $is_me; $value_1760685816720['zhaopian'] = get_files_url($value_1760685816720['zhaopian']); } $return_data = $query_16; } catch (\Exception $e){ $msg = $e->getMessage(); $code = $e->getCode(); return ApiReturn::r($code, [], $msg); } return ApiReturn::r(1, $return_data, lang('请求联系人成功')); }
最新发布
11-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值