Passed
Push — 5.2 ( 2b6c77...94f706 )
by liu
02:37
created

Connection::free()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

616
                return $this->connect(/** @scrutinizer ignore-type */ $autoConnection, $linkNum);
Loading history...
617
            } else {
618
                throw $e;
619
            }
620
        }
621
    }
622
623
    /**
624
     * 释放查询结果
625
     * @access public
626
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
627
    public function free(): void
628
    {
629
        $this->PDOStatement = null;
630
    }
631
632
    /**
633
     * 获取PDO对象
634
     * @access public
635
     * @return \PDO|false
636
     */
637
    public function getPdo()
638
    {
639
        if (!$this->linkID) {
640
            return false;
641
        }
642
643
        return $this->linkID;
644
    }
645
646
    /**
647
     * 执行查询 使用生成器返回数据
648
     * @access public
649
     * @param  Query        $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
650
     * @param  string       $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
651
     * @param  array        $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
652
     * @param  \think\Model $model 模型对象实例
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
653
     * @param  array        $condition 查询条件
654
     * @return \Generator
655
     */
656
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
657
    {
658
        $this->queryPDOStatement($query, $sql, $bind);
659
660
        // 返回结果集
661
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
662
            if ($model) {
663
                yield $model->newInstance($result, true, $condition);
664
            } else {
665
                yield $result;
666
            }
667
        }
668
    }
669
670
    /**
671
     * 执行查询 返回数据集
672
     * @access public
673
     * @param  Query  $query 查询对象
674
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
675
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
676
     * @param  bool   $cache 是否支持缓存
677
     * @return array
678
     * @throws BindParamException
679
     * @throws \PDOException
680
     * @throws \Exception
681
     * @throws \Throwable
682
     */
683
    public function query(Query $query, string $sql, array $bind = [], bool $cache = false): array
684
    {
685
        // 分析查询表达式
686
        $options = $query->parseOptions();
687
688
        if ($cache && !empty($options['cache'])) {
689
            $cacheItem = $this->parseCache($query, $options['cache']);
690
            $resultSet = $this->cache->get($cacheItem->getKey());
691
692
            if (false !== $resultSet) {
693
                return $resultSet;
694
            }
695
        }
696
697
        $master    = !empty($options['master']) ? true : false;
698
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
699
700
        $this->getPDOStatement($sql, $bind, $master, $procedure);
701
702
        $resultSet = $this->getResult($procedure);
703
704
        if (isset($cacheItem) && $resultSet) {
705
            // 缓存数据集
706
            $cacheItem->set($resultSet);
707
            $this->cacheData($cacheItem);
708
        }
709
710
        return $resultSet;
711
    }
712
713
    /**
714
     * 执行查询但只返回PDOStatement对象
715
     * @access public
716
     * @param  Query $query 查询对象
717
     * @return \PDOStatement
718
     */
719
    public function pdo(Query $query): PDOStatement
720
    {
721
        $bind = $query->getBind();
722
        // 生成查询SQL
723
        $sql = $this->builder->select($query);
724
725
        return $this->queryPDOStatement($query, $sql, $bind);
726
    }
727
728
    /**
729
     * 执行查询但只返回PDOStatement对象
730
     * @access public
731
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
732
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
733
     * @param  bool   $master 是否在主服务器读操作
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
734
     * @param  bool   $procedure 是否为存储过程调用
735
     * @return PDOStatement
736
     * @throws BindParamException
737
     * @throws \PDOException
738
     * @throws \Exception
739
     * @throws \Throwable
740
     */
741
    public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
742
    {
743
        $this->initConnect($master);
744
745
        // 记录SQL语句
746
        $this->queryStr = $sql;
747
748
        $this->bind = $bind;
749
750
        Db::updateQueryTimes();
0 ignored issues
show
Bug introduced by
The method updateQueryTimes() does not exist on think\facade\Db. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

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

750
        Db::/** @scrutinizer ignore-call */ 
751
            updateQueryTimes();
Loading history...
751
752
        try {
753
            // 调试开始
754
            $this->debug(true);
755
756
            // 预处理
757
            $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...
758
759
            // 参数绑定
760
            if ($procedure) {
761
                $this->bindParam($bind);
762
            } else {
763
                $this->bindValue($bind);
764
            }
765
766
            // 执行查询
767
            $this->PDOStatement->execute();
768
769
            // 调试结束
770
            $this->debug(false, '', $master);
771
772
            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...
773
        } catch (\Throwable | \Exception $e) {
774
            if ($this->isBreak($e)) {
775
                return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
776
            }
777
778
            if ($e instanceof \PDOException) {
779
                throw new PDOException($e, $this->config, $this->getLastsql());
780
            } else {
781
                throw $e;
782
            }
783
        }
784
    }
785
786
    /**
787
     * 执行语句
788
     * @access public
789
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
790
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
791
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
792
     * @param  bool   $origin 是否原生查询
793
     * @return int
794
     * @throws BindParamException
795
     * @throws \PDOException
796
     * @throws \Exception
797
     * @throws \Throwable
798
     */
799
    public function execute(Query $query, string $sql, array $bind = [], bool $origin = false): int
800
    {
801
        $this->queryPDOStatement($query->master(true), $sql, $bind);
802
803
        if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
804
            Db::readMaster($query->getTable());
0 ignored issues
show
Unused Code introduced by
The call to think\facade\Db::readMaster() has too many arguments starting with $query->getTable(). ( Ignorable by Annotation )

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

804
            Db::/** @scrutinizer ignore-call */ 
805
                readMaster($query->getTable());

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