Completed
Push — 6.0 ( 12bd4b...3afe85 )
by liu
03:26
created

Connection::isBreak()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

600
                return $this->connect(/** @scrutinizer ignore-type */ $autoConnection, $linkNum);
Loading history...
601
            } else {
602
                throw $e;
603
            }
604
        }
605
    }
606
607
    /**
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...
608
     * 创建PDO实例
609
     * @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...
610
     * @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...
611
     * @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...
612
     * @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...
613
     * @return PDO
614
     */
615
    protected function createPdo($dsn, $username, $password, $params)
616
    {
617
        return new PDO($dsn, $username, $password, $params);
618
    }
619
620
    /**
621
     * 释放查询结果
622
     * @access public
623
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
624
    public function free(): void
625
    {
626
        $this->PDOStatement = null;
627
    }
628
629
    /**
630
     * 获取PDO对象
631
     * @access public
632
     * @return \PDO|false
633
     */
634
    public function getPdo()
635
    {
636
        if (!$this->linkID) {
637
            return false;
638
        }
639
640
        return $this->linkID;
641
    }
642
643
    /**
644
     * 执行查询 使用生成器返回数据
645
     * @access public
646
     * @param  Query        $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
647
     * @param  string       $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
648
     * @param  array        $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
649
     * @param  \think\Model $model 模型对象实例
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
650
     * @param  array        $condition 查询条件
651
     * @return \Generator
652
     */
653
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
654
    {
655
        $this->queryPDOStatement($query, $sql, $bind);
656
657
        // 返回结果集
658
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
659
            if ($model) {
660
                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

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