Completed
Push — 6.0 ( 7f1370...dfc5cb )
by liu
03:40
created

Connection::aggregate()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 5
nc 8
nop 4
dl 0
loc 11
rs 9.6111
c 0
b 0
f 0
1
<?php
2
// +----------------------------------------------------------------------
1 ignored issue
show
Coding Style introduced by
You must use "/**" style comments for a file comment
Loading history...
3
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: liu21st <[email protected]>
10
// +----------------------------------------------------------------------
11
declare (strict_types = 1);
12
13
namespace think\db;
14
15
use InvalidArgumentException;
16
use PDO;
17
use PDOStatement;
18
use think\App;
19
use think\Cache;
20
use think\cache\CacheItem;
21
use think\Container;
22
use think\Db;
23
use think\db\exception\BindParamException;
24
use think\Exception;
25
use think\exception\PDOException;
26
use think\Log;
27
28
abstract class Connection
1 ignored issue
show
Coding Style introduced by
Missing doc comment for class Connection
Loading history...
29
{
30
    const PARAM_FLOAT = 21;
31
32
    /**
33
     * 数据库连接实例
34
     * @var array
35
     */
36
    protected static $instance = [];
37
38
    /**
39
     * PDO操作实例
40
     * @var PDOStatement
41
     */
42
    protected $PDOStatement;
43
44
    /**
45
     * 当前SQL指令
46
     * @var string
47
     */
48
    protected $queryStr = '';
49
50
    /**
51
     * 返回或者影响记录数
52
     * @var int
53
     */
54
    protected $numRows = 0;
55
56
    /**
57
     * 事务指令数
58
     * @var int
59
     */
60
    protected $transTimes = 0;
61
62
    /**
63
     * 错误信息
64
     * @var string
65
     */
66
    protected $error = '';
67
68
    /**
69
     * 数据库连接ID 支持多个连接
70
     * @var PDO[]
71
     */
72
    protected $links = [];
73
74
    /**
75
     * 当前连接ID
76
     * @var PDO
77
     */
78
    protected $linkID;
79
80
    /**
81
     * 当前读连接ID
82
     * @var PDO
83
     */
84
    protected $linkRead;
85
86
    /**
87
     * 当前写连接ID
88
     * @var PDO
89
     */
90
    protected $linkWrite;
91
92
    /**
93
     * 查询结果类型
94
     * @var int
95
     */
96
    protected $fetchType = PDO::FETCH_ASSOC;
97
98
    /**
99
     * 字段属性大小写
100
     * @var int
101
     */
102
    protected $attrCase = PDO::CASE_LOWER;
103
104
    /**
105
     * 监听回调
106
     * @var array
107
     */
108
    protected static $event = [];
109
110
    /**
111
     * 数据表信息
112
     * @var array
113
     */
114
    protected static $info = [];
115
116
    /**
117
     * Builder类名
118
     * @var string
119
     */
120
    protected $builderClassName;
121
122
    /**
123
     * Builder对象
124
     * @var Builder
125
     */
126
    protected $builder;
127
128
    /**
129
     * Db对象
130
     * @var Db
131
     */
132
    protected $db;
133
134
    /**
135
     * 数据库连接参数配置
136
     * @var array
137
     */
138
    protected $config = [
139
        // 数据库类型
140
        'type'            => '',
141
        // 服务器地址
142
        'hostname'        => '',
143
        // 数据库名
144
        'database'        => '',
145
        // 用户名
146
        'username'        => '',
147
        // 密码
148
        'password'        => '',
149
        // 端口
150
        'hostport'        => '',
151
        // 连接dsn
152
        'dsn'             => '',
153
        // 数据库连接参数
154
        'params'          => [],
155
        // 数据库编码默认采用utf8
156
        'charset'         => 'utf8',
157
        // 数据库表前缀
158
        'prefix'          => '',
159
        // 数据库调试模式
160
        'debug'           => false,
161
        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
162
        'deploy'          => 0,
163
        // 数据库读写是否分离 主从式有效
164
        'rw_separate'     => false,
165
        // 读写分离后 主服务器数量
166
        'master_num'      => 1,
167
        // 指定从服务器序号
168
        'slave_no'        => '',
169
        // 模型写入后自动读取主服务器
170
        'read_master'     => false,
171
        // 是否严格检查字段是否存在
172
        'fields_strict'   => true,
173
        // 自动写入时间戳字段
174
        'auto_timestamp'  => false,
175
        // 时间字段取出后的默认时间格式
176
        'datetime_format' => 'Y-m-d H:i:s',
177
        // 是否需要进行SQL性能分析
178
        'sql_explain'     => false,
179
        // Builder类
180
        'builder'         => '',
181
        // Query类
182
        'query'           => '\\think\\db\\Query',
183
        // 是否需要断线重连
184
        'break_reconnect' => false,
185
        // 断线标识字符串
186
        'break_match_str' => [],
187
    ];
188
189
    /**
190
     * PDO连接参数
191
     * @var array
192
     */
193
    protected $params = [
194
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
195
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
196
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
197
        PDO::ATTR_STRINGIFY_FETCHES => false,
198
        PDO::ATTR_EMULATE_PREPARES  => false,
199
    ];
200
201
    /**
202
     * 参数绑定类型映射
203
     * @var array
204
     */
205
    protected $bindType = [
206
        'string'  => PDO::PARAM_STR,
207
        'str'     => PDO::PARAM_STR,
208
        'integer' => PDO::PARAM_INT,
209
        'int'     => PDO::PARAM_INT,
210
        'boolean' => PDO::PARAM_BOOL,
211
        'bool'    => PDO::PARAM_BOOL,
212
        'float'   => self::PARAM_FLOAT,
213
    ];
214
215
    /**
216
     * 服务器断线标识字符
217
     * @var array
218
     */
219
    protected $breakMatchStr = [
220
        'server has gone away',
221
        'no connection to the server',
222
        'Lost connection',
223
        'is dead or not enabled',
224
        'Error while sending',
225
        'decryption failed or bad record mac',
226
        'server closed the connection unexpectedly',
227
        'SSL connection has been closed unexpectedly',
228
        'Error writing data to the connection',
229
        'Resource deadlock avoided',
230
        'failed with errno',
231
    ];
232
233
    /**
234
     * 绑定参数
235
     * @var array
236
     */
237
    protected $bind = [];
238
239
    /**
240
     * 缓存对象
241
     * @var Cache
242
     */
243
    protected $cache;
244
245
    /**
246
     * 日志对象
247
     * @var Log
248
     */
249
    protected $log;
250
251
    /**
252
     * 架构函数 读取数据库配置信息
253
     * @access public
254
     * @param  array $config 数据库配置数组
255
     */
256
    public function __construct(array $config = [])
257
    {
258
        if (!empty($config)) {
259
            $this->config = array_merge($this->config, $config);
260
        }
261
262
        // 创建Builder对象
263
        $class = $this->getBuilderClass();
264
265
        $this->builder = new $class($this);
266
        $this->cache   = Container::pull('cache');
267
        $this->log     = Container::pull('log');
268
269
        // 执行初始化操作
270
        $this->initialize();
271
    }
272
273
    /**
274
     * 初始化
275
     * @access protected
276
     * @return void
277
     */
278
    protected function initialize(): void
279
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
280
281
    /**
282
     * 取得数据库连接类实例
283
     * @access public
284
     * @param  array       $config 连接配置
285
     * @param  bool|string $name 连接标识 true 强制重新连接
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
286
     * @return Connection
287
     * @throws Exception
288
     */
289
    public static function instance(array $config = [], $name = false)
290
    {
291
        if (false === $name) {
292
            $name = md5(serialize($config));
293
        }
294
295
        if (true === $name || !isset(self::$instance[$name])) {
296
297
            if (empty($config['type'])) {
298
                throw new InvalidArgumentException('Undefined db type');
299
            }
300
301
            if (true === $name) {
302
                $name = md5(serialize($config));
303
            }
304
305
            self::$instance[$name] = App::factory($config['type'], '\\think\\db\\connector\\', $config);
306
        }
307
308
        return self::$instance[$name];
309
    }
310
311
    /**
312
     * 获取当前连接器类对应的Builder类
313
     * @access public
314
     * @return string
315
     */
316
    public function getBuilderClass(): string
317
    {
318
        if (!empty($this->builderClassName)) {
319
            return $this->builderClassName;
320
        }
321
322
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
323
    }
324
325
    /**
326
     * 设置当前的数据库Builder对象
327
     * @access protected
328
     * @param  Builder $builder
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
329
     * @return $this
330
     */
331
    protected function setBuilder(Builder $builder)
332
    {
333
        $this->builder = $builder;
334
335
        return $this;
336
    }
337
338
    /**
339
     * 获取当前的builder实例对象
340
     * @access public
341
     * @return Builder
342
     */
343
    public function getBuilder(): Builder
344
    {
345
        return $this->builder;
346
    }
347
348
    /**
349
     * 设置当前的数据库Db对象
350
     * @access public
351
     * @param  Db $db
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
352
     * @return $this
353
     */
354
    public function setDb(Db $db)
355
    {
356
        $this->db = $db;
357
358
        return $this;
359
    }
360
361
    /**
362
     * 解析pdo连接的dsn信息
363
     * @access protected
364
     * @param  array $config 连接信息
365
     * @return string
366
     */
367
    abstract protected function parseDsn(array $config);
368
369
    /**
370
     * 取得数据表的字段信息
371
     * @access public
372
     * @param  string $tableName 数据表名称
373
     * @return array
374
     */
375
    abstract public function getFields(string $tableName);
376
377
    /**
378
     * 取得数据库的表信息
379
     * @access public
380
     * @param string $dbName 数据库名称
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
381
     * @return array
382
     */
383
    abstract public function getTables(string $dbName);
384
385
    /**
386
     * SQL性能分析
387
     * @access protected
388
     * @param  string $sql SQL语句
389
     * @return array
390
     */
391
    abstract protected function getExplain(string $sql);
392
393
    /**
394
     * 对返数据表字段信息进行大小写转换出来
395
     * @access public
396
     * @param  array $info 字段信息
397
     * @return array
398
     */
399
    public function fieldCase(array $info): array
400
    {
401
        // 字段大小写转换
402
        switch ($this->attrCase) {
403
            case PDO::CASE_LOWER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
404
                $info = array_change_key_case($info);
405
                break;
406
            case PDO::CASE_UPPER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
407
                $info = array_change_key_case($info, CASE_UPPER);
408
                break;
409
            case PDO::CASE_NATURAL:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
410
            default:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
411
                // 不做转换
412
        }
413
414
        return $info;
415
    }
416
417
    /**
418
     * 获取字段绑定类型
419
     * @access public
420
     * @param  string $type 字段类型
421
     * @return integer
422
     */
423
    public function getFieldBindType(string $type): int
424
    {
425
        if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
426
            $bind = $this->bindType[$type];
427
        } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
428
            $bind = PDO::PARAM_STR;
429
        } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
430
            $bind = self::PARAM_FLOAT;
431
        } elseif (preg_match('/(int|serial|bit)/is', $type)) {
432
            $bind = PDO::PARAM_INT;
433
        } elseif (preg_match('/bool/is', $type)) {
434
            $bind = PDO::PARAM_BOOL;
435
        } else {
436
            $bind = PDO::PARAM_STR;
437
        }
438
439
        return $bind;
440
    }
441
442
    /**
443
     * 获取数据表信息
444
     * @access public
445
     * @param  mixed  $tableName 数据表名 留空自动获取
446
     * @param  string $fetch     获取信息类型 包括 fields type bind pk
447
     * @return mixed
448
     */
449
    public function getTableInfo($tableName, string $fetch = '')
450
    {
451
        if (is_array($tableName)) {
452
            $tableName = key($tableName) ?: current($tableName);
453
        }
454
455
        if (strpos($tableName, ',')) {
456
            // 多表不获取字段信息
457
            return [];
458
        }
459
460
        // 修正子查询作为表名的问题
461
        if (strpos($tableName, ')')) {
462
            return [];
463
        }
464
465
        list($tableName) = explode(' ', $tableName);
466
467
        if (!strpos($tableName, '.')) {
468
            $schema = $this->getConfig('database') . '.' . $tableName;
469
        } else {
470
            $schema = $tableName;
471
        }
472
473
        if (!isset(self::$info[$schema])) {
474
            // 读取缓存
475
            $cacheFile = Container::pull('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php';
476
477
            if (!$this->config['debug'] && is_file($cacheFile)) {
478
                $info = include $cacheFile;
479
            } else {
480
                $info = $this->getFields($tableName);
481
            }
482
483
            $fields = array_keys($info);
484
            $bind   = $type   = [];
485
486
            foreach ($info as $key => $val) {
487
                // 记录字段类型
488
                $type[$key] = $val['type'];
489
                $bind[$key] = $this->getFieldBindType($val['type']);
490
491
                if (!empty($val['primary'])) {
492
                    $pk[] = $key;
493
                }
494
            }
495
496
            if (isset($pk)) {
497
                // 设置主键
498
                $pk = count($pk) > 1 ? $pk : $pk[0];
499
            } else {
500
                $pk = null;
501
            }
502
503
            self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
504
        }
505
506
        return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema];
507
    }
508
509
    /**
510
     * 获取数据表的主键
511
     * @access public
512
     * @param  mixed $tableName 数据表名
513
     * @return string|array
514
     */
515
    public function getPk($tableName)
516
    {
517
        return $this->getTableInfo($tableName, 'pk');
518
    }
519
520
    /**
521
     * 获取数据表字段信息
522
     * @access public
523
     * @param  mixed $tableName 数据表名
524
     * @return array
525
     */
526
    public function getTableFields($tableName): array
527
    {
528
        return $this->getTableInfo($tableName, 'fields');
529
    }
530
531
    /**
532
     * 获取数据表字段类型
533
     * @access public
534
     * @param  mixed $tableName 数据表名
535
     * @param  string $field    字段名
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
536
     * @return array|string
537
     */
538
    public function getFieldsType($tableName, string $field = null)
539
    {
540
        $result = $this->getTableInfo($tableName, 'type');
541
542
        if ($field && isset($result[$field])) {
543
            return $result[$field];
544
        }
545
546
        return $result;
547
    }
548
549
    /**
550
     * 获取数据表绑定信息
551
     * @access public
552
     * @param  mixed $tableName 数据表名
553
     * @return array
554
     */
555
    public function getFieldsBind($tableName): array
556
    {
557
        return $this->getTableInfo($tableName, 'bind');
558
    }
559
560
    /**
561
     * 获取数据库的配置参数
562
     * @access public
563
     * @param  string $config 配置名称
564
     * @return mixed
565
     */
566
    public function getConfig(string $config = '')
567
    {
568
        if ('' === $config) {
569
            return $this->config;
570
        }
571
        return $this->config[$config] ?? null;
572
    }
573
574
    /**
575
     * 设置数据库的配置参数
576
     * @access public
577
     * @param  array $config 配置
578
     * @return void
579
     */
580
    public function setConfig(array $config): void
581
    {
582
        $this->config = array_merge($this->config, $config);
583
    }
584
585
    /**
586
     * 连接数据库方法
587
     * @access public
588
     * @param  array      $config 连接参数
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter name; 1 found
Loading history...
589
     * @param  integer    $linkNum 连接序号
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter name; 1 found
Loading history...
590
     * @param  array|bool $autoConnection 是否自动连接主数据库(用于分布式)
591
     * @return PDO
592
     * @throws Exception
593
     */
594
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
595
    {
596
        if (isset($this->links[$linkNum])) {
597
            return $this->links[$linkNum];
598
        }
599
600
        if (empty($config)) {
601
            $config = $this->config;
602
        } else {
603
            $config = array_merge($this->config, $config);
604
        }
605
606
        // 连接参数
607
        if (isset($config['params']) && is_array($config['params'])) {
608
            $params = $config['params'] + $this->params;
609
        } else {
610
            $params = $this->params;
611
        }
612
613
        // 记录当前字段属性大小写设置
614
        $this->attrCase = $params[PDO::ATTR_CASE];
615
616
        if (!empty($config['break_match_str'])) {
617
            $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
618
        }
619
620
        try {
621
            if (empty($config['dsn'])) {
622
                $config['dsn'] = $this->parseDsn($config);
623
            }
624
625
            if ($config['debug']) {
626
                $startTime             = microtime(true);
627
                $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
628
                // 记录数据库连接信息
629
                $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
630
            } else {
631
                $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
632
            }
633
634
            return $this->links[$linkNum];
635
        } catch (\PDOException $e) {
636
            if ($autoConnection) {
637
                $this->log->error($e->getMessage());
638
                return $this->connect($autoConnection, $linkNum);
0 ignored issues
show
Bug introduced by
It seems like $autoConnection can also be of type true; however, parameter $config of think\db\Connection::connect() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

638
                return $this->connect(/** @scrutinizer ignore-type */ $autoConnection, $linkNum);
Loading history...
639
            } else {
640
                throw $e;
641
            }
642
        }
643
    }
644
645
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $dsn should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $username should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $password should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $params should have a doc-comment as per coding-style.
Loading history...
646
     * 创建PDO实例
647
     * @param $dsn
1 ignored issue
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
648
     * @param $username
1 ignored issue
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
649
     * @param $password
1 ignored issue
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
650
     * @param $params
1 ignored issue
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
651
     * @return PDO
652
     */
653
    protected function createPdo($dsn, $username, $password, $params)
654
    {
655
        return new PDO($dsn, $username, $password, $params);
656
    }
657
658
    /**
659
     * 释放查询结果
660
     * @access public
661
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
662
    public function free(): void
663
    {
664
        $this->PDOStatement = null;
665
    }
666
667
    /**
668
     * 获取PDO对象
669
     * @access public
670
     * @return \PDO|false
671
     */
672
    public function getPdo()
673
    {
674
        if (!$this->linkID) {
675
            return false;
676
        }
677
678
        return $this->linkID;
679
    }
680
681
    /**
682
     * 执行查询 使用生成器返回数据
683
     * @access public
684
     * @param  Query        $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
685
     * @param  string       $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
686
     * @param  array        $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
687
     * @param  \think\Model $model 模型对象实例
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
688
     * @param  array        $condition 查询条件
689
     * @return \Generator
690
     */
691
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
692
    {
693
        $this->queryPDOStatement($query, $sql, $bind);
694
695
        // 返回结果集
696
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
697
            if ($model) {
698
                yield $model->newInstance($result, true, $condition);
0 ignored issues
show
Unused Code introduced by
The call to think\Model::newInstance() has too many arguments starting with $condition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

698
                yield $model->/** @scrutinizer ignore-call */ newInstance($result, true, $condition);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
699
            } else {
700
                yield $result;
701
            }
702
        }
703
    }
704
705
    /**
706
     * 执行查询 返回数据集
707
     * @access public
708
     * @param  Query  $query 查询对象
709
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
710
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
711
     * @param  bool   $cache 是否支持缓存
712
     * @return array
713
     * @throws BindParamException
714
     * @throws \PDOException
715
     * @throws \Exception
716
     * @throws \Throwable
717
     */
718
    public function query(Query $query, string $sql, array $bind = [], bool $cache = false): array
719
    {
720
        // 分析查询表达式
721
        $options = $query->parseOptions();
722
723
        if ($cache && !empty($options['cache'])) {
724
            $cacheItem = $this->parseCache($query, $options['cache']);
725
            $resultSet = $this->cache->get($cacheItem->getKey());
726
727
            if (false !== $resultSet) {
728
                return $resultSet;
729
            }
730
        }
731
732
        $master    = !empty($options['master']) ? true : false;
733
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
734
735
        $this->getPDOStatement($sql, $bind, $master, $procedure);
736
737
        $resultSet = $this->getResult($procedure);
738
739
        if (isset($cacheItem) && $resultSet) {
740
            // 缓存数据集
741
            $cacheItem->set($resultSet);
742
            $this->cacheData($cacheItem);
743
        }
744
745
        return $resultSet;
746
    }
747
748
    /**
749
     * 执行查询但只返回PDOStatement对象
750
     * @access public
751
     * @param  Query $query 查询对象
752
     * @return \PDOStatement
753
     */
754
    public function pdo(Query $query): PDOStatement
755
    {
756
        $bind = $query->getBind();
757
        // 生成查询SQL
758
        $sql = $this->builder->select($query);
759
760
        return $this->queryPDOStatement($query, $sql, $bind);
761
    }
762
763
    /**
764
     * 执行查询但只返回PDOStatement对象
765
     * @access public
766
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
767
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
768
     * @param  bool   $master 是否在主服务器读操作
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
769
     * @param  bool   $procedure 是否为存储过程调用
770
     * @return PDOStatement
771
     * @throws BindParamException
772
     * @throws \PDOException
773
     * @throws \Exception
774
     * @throws \Throwable
775
     */
776
    public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
777
    {
778
        $this->initConnect($master);
779
780
        // 记录SQL语句
781
        $this->queryStr = $sql;
782
783
        $this->bind = $bind;
784
785
        $this->db->updateQueryTimes();
786
787
        try {
788
            // 调试开始
789
            $this->debug(true);
790
791
            // 预处理
792
            $this->PDOStatement = $this->linkID->prepare($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->linkID->prepare($sql) can also be of type boolean. However, the property $PDOStatement is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
793
794
            // 参数绑定
795
            if ($procedure) {
796
                $this->bindParam($bind);
797
            } else {
798
                $this->bindValue($bind);
799
            }
800
801
            // 执行查询
802
            $this->PDOStatement->execute();
803
804
            // 调试结束
805
            $this->debug(false, '', $master);
806
807
            return $this->PDOStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->PDOStatement could return the type boolean which is incompatible with the type-hinted return PDOStatement. Consider adding an additional type-check to rule them out.
Loading history...
808
        } catch (\Throwable | \Exception $e) {
809
            if ($this->isBreak($e)) {
810
                return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
811
            }
812
813
            if ($e instanceof \PDOException) {
814
                throw new PDOException($e, $this->config, $this->getLastsql());
815
            } else {
816
                throw $e;
817
            }
818
        }
819
    }
820
821
    /**
822
     * 执行语句
823
     * @access public
824
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
825
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
826
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
827
     * @param  bool   $origin 是否原生查询
828
     * @return int
829
     * @throws BindParamException
830
     * @throws \PDOException
831
     * @throws \Exception
832
     * @throws \Throwable
833
     */
834
    public function execute(Query $query, string $sql, array $bind = [], bool $origin = false): int
835
    {
836
        $this->queryPDOStatement($query->master(true), $sql, $bind);
837
838
        if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
839
            Db::readMaster($query->getTable());
840
        }
841
842
        $this->numRows = $this->PDOStatement->rowCount();
843
844
        return $this->numRows;
845
    }
846
847
    protected function queryPDOStatement(Query $query, string $sql, array $bind = []): PDOStatement
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function queryPDOStatement()
Loading history...
848
    {
849
        $options   = $query->parseOptions();
850
        $master    = !empty($options['master']) ? true : false;
851
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
852
853
        return $this->getPDOStatement($sql, $bind, $master, $procedure);
854
    }
855
856
    /**
857
     * 查找单条记录
858
     * @access public
859
     * @param  Query $query 查询对象
860
     * @return array
861
     * @throws DbException
862
     * @throws ModelNotFoundException
863
     * @throws DataNotFoundException
864
     */
865
    public function find(Query $query): array
866
    {
867
        // 分析查询表达式
868
        $options = $query->parseOptions();
869
870
        if (!empty($options['cache'])) {
871
            // 判断查询缓存
872
            $cacheItem = $this->parseCache($query, $options['cache']);
873
            $key       = $cacheItem->getKey();
874
        }
875
876
        if (isset($key)) {
877
            $result = $this->cache->get($key);
878
879
            if (false !== $result) {
880
                return $result;
881
            }
882
        }
883
884
        // 生成查询SQL
885
        $sql = $this->builder->select($query, true);
886
887
        // 事件回调
888
        $result = $query->trigger('before_find');
889
890
        if (!$result) {
891
            // 执行查询
892
            $resultSet = $this->query($query, $sql, $query->getBind());
893
894
            $result = $resultSet[0] ?? [];
895
        }
896
897
        if (isset($cacheItem) && $result) {
898
            // 缓存数据
899
            $cacheItem->set($result);
900
            $this->cacheData($cacheItem);
901
        }
902
903
        return $result;
904
    }
905
906
    /**
907
     * 使用游标查询记录
908
     * @access public
909
     * @param  Query $query 查询对象
910
     * @return \Generator
911
     */
912
    public function cursor(Query $query)
913
    {
914
        // 分析查询表达式
915
        $options = $query->parseOptions();
916
917
        // 生成查询SQL
918
        $sql = $this->builder->select($query);
919
920
        $condition = $options['where']['AND'] ?? null;
921
922
        // 执行查询操作
923
        return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
924
    }
925
926
    /**
927
     * 查找记录
928
     * @access public
929
     * @param  Query $query 查询对象
930
     * @return array
931
     * @throws DbException
932
     * @throws ModelNotFoundException
933
     * @throws DataNotFoundException
934
     */
935
    public function select(Query $query): array
936
    {
937
        // 分析查询表达式
938
        $options = $query->parseOptions();
939
940
        if (!empty($options['cache'])) {
941
            $cacheItem = $this->parseCache($query, $options['cache']);
942
            $resultSet = $this->getCacheData($cacheItem);
943
944
            if (false !== $resultSet) {
945
                return $resultSet;
946
            }
947
        }
948
949
        // 生成查询SQL
950
        $sql = $this->builder->select($query);
951
952
        $resultSet = $query->trigger('before_select');
953
954
        if (!$resultSet) {
955
            // 执行查询操作
956
            $resultSet = $this->query($query, $sql, $query->getBind());
957
        }
958
959
        if (isset($cacheItem) && false !== $resultSet) {
960
            // 缓存数据集
961
            $cacheItem->set($resultSet);
962
            $this->cacheData($cacheItem);
963
        }
964
965
        return $resultSet;
966
    }
967
968
    /**
969
     * 插入记录
970
     * @access public
971
     * @param  Query   $query        查询对象
972
     * @param  boolean $getLastInsID 返回自增主键
973
     * @return mixed
974
     */
975
    public function insert(Query $query, bool $getLastInsID = false)
976
    {
977
        // 分析查询表达式
978
        $options = $query->parseOptions();
979
980
        // 生成SQL语句
981
        $sql = $this->builder->insert($query);
982
983
        // 执行操作
984
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
985
986
        if ($result) {
987
            $sequence  = $options['sequence'] ?? null;
988
            $lastInsId = $this->getLastInsID($sequence);
989
990
            $data = $options['data'];
991
992
            if ($lastInsId) {
993
                $pk = $query->getPk();
994
                if (is_string($pk)) {
995
                    $data[$pk] = $lastInsId;
996
                }
997
            }
998
999
            $query->setOption('data', $data);
1000
1001
            $query->trigger('after_insert');
1002
1003
            if ($getLastInsID) {
1004
                return $lastInsId;
1005
            }
1006
        }
1007
1008
        return $result;
1009
    }
1010
1011
    /**
1012
     * 批量插入记录
1013
     * @access public
1014
     * @param  Query   $query   查询对象
1015
     * @param  mixed   $dataSet 数据集
1016
     * @param  integer $limit   每次写入数据限制
1017
     * @return integer
1018
     * @throws \Exception
1019
     * @throws \Throwable
1020
     */
1021
    public function insertAll(Query $query, array $dataSet = [], int $limit = 0): int
1022
    {
1023
        if (!is_array(reset($dataSet))) {
1024
            return 0;
1025
        }
1026
1027
        $query->parseOptions();
1028
1029
        if ($limit) {
1030
            // 分批写入 自动启动事务支持
1031
            $this->startTrans();
1032
1033
            try {
1034
                $array = array_chunk($dataSet, $limit, true);
1035
                $count = 0;
1036
1037
                foreach ($array as $item) {
1038
                    $sql = $this->builder->insertAll($query, $item);
1039
                    $count += $this->execute($query, $sql, $query->getBind());
1040
                }
1041
1042
                // 提交事务
1043
                $this->commit();
1044
            } catch (\Exception | \Throwable $e) {
1045
                $this->rollback();
1046
                throw $e;
1047
            }
1048
1049
            return $count;
1050
        }
1051
1052
        $sql = $this->builder->insertAll($query, $dataSet);
1053
1054
        return $this->execute($query, $sql, $query->getBind());
1055
    }
1056
1057
    /**
1058
     * 通过Select方式插入记录
1059
     * @access public
1060
     * @param  Query  $query  查询对象
1061
     * @param  array  $fields 要插入的数据表字段名
1062
     * @param  string $table  要插入的数据表名
1063
     * @return integer
1064
     * @throws PDOException
1065
     */
1066
    public function selectInsert(Query $query, array $fields, string $table): int
1067
    {
1068
        // 分析查询表达式
1069
        $sql = $this->builder->selectInsert($query, $fields, $table);
1070
1071
        return $this->execute($query, $sql, $query->getBind());
1072
    }
1073
1074
    /**
1075
     * 更新记录
1076
     * @access public
1077
     * @param  Query $query 查询对象
1078
     * @return integer
1079
     * @throws Exception
1080
     * @throws PDOException
1081
     */
1082
    public function update(Query $query): int
1083
    {
1084
        $options = $query->parseOptions();
1085
1086
        if (isset($options['cache'])) {
1087
            $cacheItem = $this->parseCache($query, $options['cache']);
1088
            $key       = $cacheItem->getKey();
1089
        }
1090
1091
        // 生成UPDATE SQL语句
1092
        $sql = $this->builder->update($query);
1093
1094
        // 检测缓存
1095
        if (isset($key) && $this->cache->get($key)) {
1096
            // 删除缓存
1097
            $this->cache->delete($key);
1098
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1099
            $this->cache->tag($cacheItem->getTag())->clear();
1100
        }
1101
1102
        // 执行操作
1103
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
1104
1105
        if ($result) {
1106
            $query->trigger('after_update');
1107
        }
1108
1109
        return $result;
1110
    }
1111
1112
    /**
1113
     * 删除记录
1114
     * @access public
1115
     * @param  Query $query 查询对象
1116
     * @return int
1117
     * @throws Exception
1118
     * @throws PDOException
1119
     */
1120
    public function delete(Query $query): int
1121
    {
1122
        // 分析查询表达式
1123
        $options = $query->parseOptions();
1124
1125
        if (isset($options['cache'])) {
1126
            $cacheItem = $this->parseCache($query, $options['cache']);
1127
            $key       = $cacheItem->getKey();
1128
        }
1129
1130
        // 生成删除SQL语句
1131
        $sql = $this->builder->delete($query);
1132
1133
        // 检测缓存
1134
        if (isset($key) && $this->cache->get($key)) {
1135
            // 删除缓存
1136
            $this->cache->delete($key);
1137
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1138
            $this->cache->tag($cacheItem->getTag())->clear();
1139
        }
1140
1141
        // 执行操作
1142
        $result = $this->execute($query, $sql, $query->getBind());
1143
1144
        if ($result) {
1145
            $query->trigger('after_delete');
1146
        }
1147
1148
        return $result;
1149
    }
1150
1151
    /**
1152
     * 得到某个字段的值
1153
     * @access public
1154
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
1155
     * @param  string $field   字段名
1156
     * @param  mixed  $default   默认值
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1157
     * @return mixed
1158
     */
1159
    public function value(Query $query, string $field, $default = null)
1160
    {
1161
        $options = $query->parseOptions();
1162
1163
        if (isset($options['field'])) {
1164
            $query->removeOption('field');
1165
        }
1166
1167
        $query->setOption('field', (array) $field);
1168
1169
        if (!empty($options['cache'])) {
1170
            $cacheItem = $this->parseCache($query, $options['cache']);
1171
            $result    = $this->getCacheData($cacheItem);
1172
1173
            if (false !== $result) {
1174
                return $result;
1175
            }
1176
        }
1177
1178
        // 生成查询SQL
1179
        $sql = $this->builder->select($query, true);
1180
1181
        if (isset($options['field'])) {
1182
            $query->setOption('field', $options['field']);
1183
        } else {
1184
            $query->removeOption('field');
1185
        }
1186
1187
        // 执行查询操作
1188
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1189
1190
        $result = $pdo->fetchColumn();
1191
1192
        if (isset($cacheItem) && false !== $result) {
1193
            // 缓存数据
1194
            $cacheItem->set($result);
1195
            $this->cacheData($cacheItem);
1196
        }
1197
1198
        return false !== $result ? $result : $default;
1199
    }
1200
1201
    /**
1202
     * 得到某个字段的值
1203
     * @access public
1204
     * @param  Query  $query     查询对象
1205
     * @param  string $aggregate 聚合方法
1206
     * @param  mixed  $field     字段名
1207
     * @param  bool   $force     强制转为数字类型
1208
     * @return mixed
1209
     */
1210
    public function aggregate(Query $query, string $aggregate, $field, bool $force = false)
1211
    {
1212
        if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
1213
            list($distinct, $field) = explode(' ', $field);
1214
        }
1215
1216
        $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
1217
1218
        $result = $this->value($query, $field, 0);
1219
1220
        return $force ? (float) $result : $result;
1221
    }
1222
1223
    /**
1224
     * 得到某个列的数组
1225
     * @access public
1226
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1227
     * @param  string $column 字段名 多个字段用逗号分隔
1228
     * @param  string $key   索引
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 3 found
Loading history...
1229
     * @return array
1230
     */
1231
    public function column(Query $query, string $column, string $key = ''): array
1232
    {
1233
        $options = $query->parseOptions();
1234
1235
        if (isset($options['field'])) {
1236
            $query->removeOption('field');
1237
        }
1238
1239
        if ($key && '*' != $column) {
1240
            $field = $key . ',' . $column;
1241
        } else {
1242
            $field = $column;
1243
        }
1244
1245
        $field = array_map('trim', explode(',', $field));
1246
1247
        $query->setOption('field', $field);
1248
1249
        if (!empty($options['cache'])) {
1250
            // 判断查询缓存
1251
            $cacheItem = $this->parseCache($query, $options['cache']);
1252
            $result    = $this->getCacheData($cacheItem);
1253
1254
            if (false !== $result) {
1255
                return $result;
1256
            }
1257
        }
1258
1259
        // 生成查询SQL
1260
        $sql = $this->builder->select($query);
1261
1262
        if (isset($options['field'])) {
1263
            $query->setOption('field', $options['field']);
1264
        } else {
1265
            $query->removeOption('field');
1266
        }
1267
1268
        // 执行查询操作
1269
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1270
1271
        $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
1272
1273
        if (empty($resultSet)) {
1274
            $result = [];
1275
        } elseif (('*' == $column || strpos($column, ',')) && $key) {
1276
            $result = array_column($resultSet, null, $key);
1277
        } else {
1278
            $fields = array_keys($resultSet[0]);
1279
            $key    = $key ?: array_shift($fields);
1280
1281
            if (strpos($key, '.')) {
1282
                list($alias, $key) = explode('.', $key);
1283
            }
1284
1285
            $result = array_column($resultSet, $column, $key);
1286
        }
1287
1288
        if (isset($cacheItem)) {
1289
            // 缓存数据
1290
            $cacheItem->set($result);
1291
            $this->cacheData($cacheItem);
1292
        }
1293
1294
        return $result;
1295
    }
1296
1297
    /**
1298
     * 根据参数绑定组装最终的SQL语句 便于调试
1299
     * @access public
1300
     * @param  string $sql 带参数绑定的sql语句
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1301
     * @param  array  $bind 参数绑定列表
1302
     * @return string
1303
     */
1304
    public function getRealSql(string $sql, array $bind = []): string
1305
    {
1306
        foreach ($bind as $key => $val) {
1307
            $value = is_array($val) ? $val[0] : $val;
1308
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
1309
1310
            if (self::PARAM_FLOAT == $type) {
1311
                $value = (float) $value;
1312
            } elseif (PDO::PARAM_STR == $type && is_string($value)) {
1313
                $value = '\'' . addslashes($value) . '\'';
1314
            } elseif (PDO::PARAM_INT == $type && '' === $value) {
1315
                $value = 0;
1316
            }
1317
1318
            // 判断占位符
1319
            $sql = is_numeric($key) ?
1320
            substr_replace($sql, $value, strpos($sql, '?'), 1) :
1321
            substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
1322
        }
1323
1324
        return rtrim($sql);
1325
    }
1326
1327
    /**
1328
     * 参数绑定
1329
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
1330
     * 或者 ['value',123] 对应问号占位符
1331
     * @access public
1332
     * @param  array $bind 要绑定的参数列表
1333
     * @return void
1334
     * @throws BindParamException
1335
     */
1336
    protected function bindValue(array $bind = []): void
1337
    {
1338
        foreach ($bind as $key => $val) {
1339
            // 占位符
1340
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1341
1342
            if (is_array($val)) {
1343
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
1344
                    $val[0] = 0;
1345
                } elseif (self::PARAM_FLOAT == $val[1]) {
1346
                    $val[0] = (float) $val[0];
1347
                    $val[1] = PDO::PARAM_STR;
1348
                }
1349
1350
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
1351
            } else {
1352
                $result = $this->PDOStatement->bindValue($param, $val);
1353
            }
1354
1355
            if (!$result) {
1356
                throw new BindParamException(
1357
                    "Error occurred  when binding parameters '{$param}'",
1358
                    $this->config,
1359
                    $this->getLastsql(),
1360
                    $bind
1361
                );
1362
            }
1363
        }
1364
    }
1365
1366
    /**
1367
     * 存储过程的输入输出参数绑定
1368
     * @access public
1369
     * @param  array $bind 要绑定的参数列表
1370
     * @return void
1371
     * @throws BindParamException
1372
     */
1373
    protected function bindParam(array $bind): void
1374
    {
1375
        foreach ($bind as $key => $val) {
1376
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1377
1378
            if (is_array($val)) {
1379
                array_unshift($val, $param);
1380
                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
1381
            } else {
1382
                $result = $this->PDOStatement->bindValue($param, $val);
1383
            }
1384
1385
            if (!$result) {
1386
                $param = array_shift($val);
1387
1388
                throw new BindParamException(
1389
                    "Error occurred  when binding parameters '{$param}'",
1390
                    $this->config,
1391
                    $this->getLastsql(),
1392
                    $bind
1393
                );
1394
            }
1395
        }
1396
    }
1397
1398
    /**
1399
     * 获得数据集数组
1400
     * @access protected
1401
     * @param  bool $procedure 是否存储过程
1402
     * @return array
1403
     */
1404
    protected function getResult(bool $procedure = false): array
1405
    {
1406
        if ($procedure) {
1407
            // 存储过程返回结果
1408
            return $this->procedure();
1409
        }
1410
1411
        $result = $this->PDOStatement->fetchAll($this->fetchType);
1412
1413
        $this->numRows = count($result);
1414
1415
        return $result;
1416
    }
1417
1418
    /**
1419
     * 获得存储过程数据集
1420
     * @access protected
1421
     * @return array
1422
     */
1423
    protected function procedure(): array
1424
    {
1425
        $item = [];
1426
1427
        do {
1428
            $result = $this->getResult();
1429
            if (!empty($result)) {
1430
                $item[] = $result;
1431
            }
1432
        } while ($this->PDOStatement->nextRowset());
1433
1434
        $this->numRows = count($item);
1435
1436
        return $item;
1437
    }
1438
1439
    /**
1440
     * 执行数据库事务
1441
     * @access public
1442
     * @param  callable $callback 数据操作方法回调
1443
     * @return mixed
1444
     * @throws PDOException
1445
     * @throws \Exception
1446
     * @throws \Throwable
1447
     */
1448
    public function transaction(callable $callback)
1449
    {
1450
        $this->startTrans();
1451
1452
        try {
1453
            $result = null;
1454
            if (is_callable($callback)) {
1455
                $result = call_user_func_array($callback, [$this]);
1456
            }
1457
1458
            $this->commit();
1459
            return $result;
1460
        } catch (\Exception | \Throwable $e) {
1461
            $this->rollback();
1462
            throw $e;
1463
        }
1464
    }
1465
1466
    /**
1467
     * 启动事务
1468
     * @access public
1469
     * @return void
1470
     * @throws \PDOException
1471
     * @throws \Exception
1472
     */
1473
    public function startTrans(): void
1474
    {
1475
        $this->initConnect(true);
1476
1477
        ++$this->transTimes;
1478
1479
        try {
1480
            if (1 == $this->transTimes) {
1481
                $this->linkID->beginTransaction();
1482
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1483
                $this->linkID->exec(
1484
                    $this->parseSavepoint('trans' . $this->transTimes)
1485
                );
1486
            }
1487
        } catch (\Exception $e) {
1488
            if ($this->isBreak($e)) {
1489
                --$this->transTimes;
1490
                $this->close()->startTrans();
1491
            }
1492
            throw $e;
1493
        }
1494
    }
1495
1496
    /**
1497
     * 用于非自动提交状态下面的查询提交
1498
     * @access public
1499
     * @return void
1500
     * @throws PDOException
1501
     */
1502
    public function commit(): void
1503
    {
1504
        $this->initConnect(true);
1505
1506
        if (1 == $this->transTimes) {
1507
            $this->linkID->commit();
1508
        }
1509
1510
        --$this->transTimes;
1511
    }
1512
1513
    /**
1514
     * 事务回滚
1515
     * @access public
1516
     * @return void
1517
     * @throws PDOException
1518
     */
1519
    public function rollback(): void
1520
    {
1521
        $this->initConnect(true);
1522
1523
        if (1 == $this->transTimes) {
1524
            $this->linkID->rollBack();
1525
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1526
            $this->linkID->exec(
1527
                $this->parseSavepointRollBack('trans' . $this->transTimes)
1528
            );
1529
        }
1530
1531
        $this->transTimes = max(0, $this->transTimes - 1);
1532
    }
1533
1534
    /**
1535
     * 是否支持事务嵌套
1536
     * @return bool
1537
     */
1538
    protected function supportSavepoint(): bool
1539
    {
1540
        return false;
1541
    }
1542
1543
    /**
1544
     * 生成定义保存点的SQL
1545
     * @access protected
1546
     * @param  string $name 标识
1547
     * @return string
1548
     */
1549
    protected function parseSavepoint(string $name): string
1550
    {
1551
        return 'SAVEPOINT ' . $name;
1552
    }
1553
1554
    /**
1555
     * 生成回滚到保存点的SQL
1556
     * @access protected
1557
     * @param  string $name 标识
1558
     * @return string
1559
     */
1560
    protected function parseSavepointRollBack(string $name): string
1561
    {
1562
        return 'ROLLBACK TO SAVEPOINT ' . $name;
1563
    }
1564
1565
    /**
1566
     * 批处理执行SQL语句
1567
     * 批处理的指令都认为是execute操作
1568
     * @access public
1569
     * @param  Query $query        查询对象
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 8 found
Loading history...
1570
     * @param  array $sqlArray   SQL批处理指令
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1571
     * @param  array $bind       参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 7 found
Loading history...
1572
     * @return bool
1573
     */
1574
    public function batchQuery(Query $query, array $sqlArray = [], array $bind = []): bool
1575
    {
1576
        // 自动启动事务支持
1577
        $this->startTrans();
1578
1579
        try {
1580
            foreach ($sqlArray as $sql) {
1581
                $this->execute($query, $sql, $bind);
1582
            }
1583
            // 提交事务
1584
            $this->commit();
1585
        } catch (\Exception $e) {
1586
            $this->rollback();
1587
            throw $e;
1588
        }
1589
1590
        return true;
1591
    }
1592
1593
    /**
1594
     * 关闭数据库(或者重新连接)
1595
     * @access public
1596
     * @return $this
1597
     */
1598
    public function close()
1599
    {
1600
        $this->linkID    = null;
1601
        $this->linkWrite = null;
1602
        $this->linkRead  = null;
1603
        $this->links     = [];
1604
1605
        $this->free();
1606
1607
        return $this;
1608
    }
1609
1610
    /**
1611
     * 是否断线
1612
     * @access protected
1613
     * @param  \PDOException|\Exception $e 异常对象
1614
     * @return bool
1615
     */
1616
    protected function isBreak($e): bool
1617
    {
1618
        if (!$this->config['break_reconnect']) {
1619
            return false;
1620
        }
1621
1622
        $error = $e->getMessage();
1623
1624
        foreach ($this->breakMatchStr as $msg) {
1625
            if (false !== stripos($error, $msg)) {
1626
                return true;
1627
            }
1628
        }
1629
1630
        return false;
1631
    }
1632
1633
    /**
1634
     * 获取最近一次查询的sql语句
1635
     * @access public
1636
     * @return string
1637
     */
1638
    public function getLastSql(): string
1639
    {
1640
        return $this->getRealSql($this->queryStr, $this->bind);
1641
    }
1642
1643
    /**
1644
     * 获取最近插入的ID
1645
     * @access public
1646
     * @param  string $sequence 自增序列名
1647
     * @return string
1648
     */
1649
    public function getLastInsID(string $sequence = null): string
1650
    {
1651
        return $this->linkID->lastInsertId($sequence);
1652
    }
1653
1654
    /**
1655
     * 获取返回或者影响的记录数
1656
     * @access public
1657
     * @return integer
1658
     */
1659
    public function getNumRows(): int
1660
    {
1661
        return $this->numRows;
1662
    }
1663
1664
    /**
1665
     * 获取最近的错误信息
1666
     * @access public
1667
     * @return string
1668
     */
1669
    public function getError(): string
1670
    {
1671
        if ($this->PDOStatement) {
1672
            $error = $this->PDOStatement->errorInfo();
1673
            $error = $error[1] . ':' . $error[2];
1674
        } else {
1675
            $error = '';
1676
        }
1677
1678
        if ('' != $this->queryStr) {
1679
            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
1680
        }
1681
1682
        return $error;
1683
    }
1684
1685
    /**
1686
     * 数据库调试 记录当前SQL及分析性能
1687
     * @access protected
1688
     * @param  boolean $start 调试开始标记 true 开始 false 结束
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1689
     * @param  string  $sql 执行的SQL语句 留空自动获取
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
1690
     * @param  bool    $master 主从标记
1691
     * @return void
1692
     */
1693
    protected function debug(bool $start, string $sql = '', bool $master = false): void
1694
    {
1695
        if (!empty($this->config['debug'])) {
1696
            // 开启数据库调试模式
1697
            $debug = Container::pull('debug');
1698
1699
            if ($start) {
1700
                $debug->remark('queryStartTime', 'time');
1701
            } else {
1702
                // 记录操作结束时间
1703
                $debug->remark('queryEndTime', 'time');
1704
                $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime');
1705
                $sql     = $sql ?: $this->getLastsql();
1706
                $result  = [];
1707
1708
                // SQL性能分析
1709
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
1710
                    $result = $this->getExplain($sql);
1711
                }
1712
1713
                // SQL监听
1714
                $this->triggerSql($sql, $runtime, $result, $master);
1715
            }
1716
        }
1717
    }
1718
1719
    /**
1720
     * 监听SQL执行
1721
     * @access public
1722
     * @param  callable $callback 回调方法
1723
     * @return void
1724
     */
1725
    public function listen(callable $callback): void
1726
    {
1727
        self::$event[] = $callback;
1728
    }
1729
1730
    /**
1731
     * 触发SQL事件
1732
     * @access protected
1733
     * @param  string $sql SQL语句
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
1734
     * @param  string $runtime SQL运行时间
1735
     * @param  mixed  $explain SQL分析
1736
     * @param  bool   $master 主从标记
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1737
     * @return void
1738
     */
1739
    protected function triggerSql(string $sql, string $runtime, array $explain = [], bool $master = false): void
1740
    {
1741
        if (!empty(self::$event)) {
1742
            foreach (self::$event as $callback) {
1743
                if (is_callable($callback)) {
1744
                    call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
1745
                }
1746
            }
1747
        } else {
1748
            if ($this->config['deploy']) {
1749
                // 分布式记录当前操作的主从
1750
                $master = $master ? 'master|' : 'slave|';
1751
            } else {
1752
                $master = '';
1753
            }
1754
1755
            // 未注册监听则记录到日志中
1756
            $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
1757
1758
            if (!empty($explain)) {
1759
                $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
1760
            }
1761
        }
1762
    }
1763
1764
    /**
1765
     * 记录SQL日志
1766
     * @access protected
1767
     * @param  string $log SQL日志信息
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1768
     * @param  string $type 日志类型
1769
     * @return void
1770
     */
1771
    protected function log($log, $type = 'sql'): void
1772
    {
1773
        if ($this->config['debug']) {
1774
            $this->log->record($log, $type);
1775
        }
1776
    }
1777
1778
    /**
1779
     * 初始化数据库连接
1780
     * @access protected
1781
     * @param  boolean $master 是否主服务器
1782
     * @return void
1783
     */
1784
    protected function initConnect(bool $master = true): void
1785
    {
1786
        if (!empty($this->config['deploy'])) {
1787
            // 采用分布式数据库
1788
            if ($master || $this->transTimes) {
1789
                if (!$this->linkWrite) {
1790
                    $this->linkWrite = $this->multiConnect(true);
1791
                }
1792
1793
                $this->linkID = $this->linkWrite;
1794
            } else {
1795
                if (!$this->linkRead) {
1796
                    $this->linkRead = $this->multiConnect(false);
1797
                }
1798
1799
                $this->linkID = $this->linkRead;
1800
            }
1801
        } elseif (!$this->linkID) {
1802
            // 默认单数据库
1803
            $this->linkID = $this->connect();
1804
        }
1805
    }
1806
1807
    /**
1808
     * 连接分布式服务器
1809
     * @access protected
1810
     * @param  boolean $master 主服务器
1811
     * @return PDO
1812
     */
1813
    protected function multiConnect(bool $master = false): PDO
1814
    {
1815
        $config = [];
1816
1817
        // 分布式数据库配置解析
1818
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1819
            $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
1820
        }
1821
1822
        // 主服务器序号
1823
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
1824
1825
        if ($this->config['rw_separate']) {
1826
            // 主从式采用读写分离
1827
            if ($master) // 主服务器写入
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...) // 主服务器写入\n {\n"
Loading history...
1828
            {
1829
                $r = $m;
1830
            } elseif (is_numeric($this->config['slave_no'])) {
1831
                // 指定服务器读
1832
                $r = $this->config['slave_no'];
1833
            } else {
1834
                // 读操作连接从服务器 每次随机连接的数据库
1835
                $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
1836
            }
1837
        } else {
1838
            // 读写操作不区分服务器 每次随机连接的数据库
1839
            $r = floor(mt_rand(0, count($config['hostname']) - 1));
1840
        }
1841
        $dbMaster = false;
1842
1843
        if ($m != $r) {
1844
            $dbMaster = [];
1845
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1846
                $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
1847
            }
1848
        }
1849
1850
        $dbConfig = [];
1851
1852
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1853
            $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
1854
        }
1855
1856
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
1857
    }
1858
1859
    /**
1860
     * 析构方法
1861
     * @access public
1862
     */
1863
    public function __destruct()
1864
    {
1865
        // 释放查询
1866
        $this->free();
1867
1868
        // 关闭连接
1869
        $this->close();
1870
    }
1871
1872
    /**
1873
     * 缓存数据
1874
     * @access protected
1875
     * @param  CacheItem $cacheItem 缓存Item
1876
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1877
    protected function cacheData(CacheItem $cacheItem): void
1878
    {
1879
        if ($cacheItem->getTag()) {
1880
            $this->cache->tag($cacheItem->getTag());
1881
        }
1882
1883
        $this->cache->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
1884
    }
1885
1886
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $cacheItem should have a doc-comment as per coding-style.
Loading history...
1887
     * 获取缓存数据
1888
     * @access protected
1889
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Doc comment for parameter $query does not match actual variable name $cacheItem
Loading history...
1890
     * @param  mixed  $cache 缓存设置
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1891
     * @param  array  $data  缓存数据
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1892
     * @param  string $key   缓存Key
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1893
     * @return mixed
1894
     */
1895
    protected function getCacheData(CacheItem $cacheItem)
1896
    {
1897
        // 判断查询缓存
1898
        return $this->cache->get($cacheItem->getKey());
1899
    }
1900
1901
    protected function parseCache(Query $query, array $cache): CacheItem
0 ignored issues
show
Coding Style introduced by
Missing doc comment for function parseCache()
Loading history...
1902
    {
1903
        list($key, $expire, $tag) = $cache;
1904
1905
        if ($key instanceof CacheItem) {
1906
            $cacheItem = $key;
1907
        } else {
1908
            if (true === $key) {
1909
                if (!empty($query->getOptions('key'))) {
1910
                    $key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
1911
                } else {
1912
                    $key = md5($this->getConfig('database') . serialize($query->getOptions()) . serialize($query->getBind(false)));
1913
                }
1914
            }
1915
1916
            $cacheItem = new CacheItem($key);
1917
            $cacheItem->expire($expire);
1918
            $cacheItem->tag($tag);
1919
        }
1920
1921
        return $cacheItem;
1922
    }
1923
1924
    /**
1925
     * 延时更新检查 返回false表示需要延时
1926
     * 否则返回实际写入的数值
1927
     * @access public
1928
     * @param  string  $type     自增或者自减
1929
     * @param  string  $guid     写入标识
1930
     * @param  integer $step     写入步进值
1931
     * @param  integer $lazyTime 延时时间(s)
1932
     * @return false|integer
1933
     */
1934
    public function lazyWrite(string $type, string $guid, int $step, int $lazyTime)
1935
    {
1936
        if (!$this->cache->has($guid . '_time')) {
1937
            // 计时开始
1938
            $this->cache->set($guid . '_time', time(), 0);
1939
            $this->cache->$type($guid, $step);
1940
        } elseif (time() > $this->cache->get($guid . '_time') + $lazyTime) {
1941
            // 删除缓存
1942
            $value = $this->cache->$type($guid, $step);
1943
            $this->cache->delete($guid);
1944
            $this->cache->delete($guid . '_time');
1945
            return 0 === $value ? false : $value;
1946
        } else {
1947
            // 更新缓存
1948
            $this->cache->$type($guid, $step);
1949
        }
1950
1951
        return false;
1952
    }
1953
}
1954