Passed
Push — 6.0 ( 56ba62...b1927d )
by liu
05:06 queued 10s
created

PDOConnection::connect()   B

Complexity

Conditions 9
Paths 177

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 26
nc 177
nop 3
dl 0
loc 43
rs 7.4138
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
/**
27
 * 数据库连接基础类
28
 */
5 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
29
abstract class PDOConnection extends Connection
30
{
31
    const PARAM_FLOAT = 21;
32
33
    /**
34
     * PDO操作实例
35
     * @var PDOStatement
36
     */
37
    protected $PDOStatement;
38
39
    /**
40
     * 当前SQL指令
41
     * @var string
42
     */
43
    protected $queryStr = '';
44
45
    /**
46
     * 事务指令数
47
     * @var int
48
     */
49
    protected $transTimes = 0;
50
51
    /**
52
     * 查询结果类型
53
     * @var int
54
     */
55
    protected $fetchType = PDO::FETCH_ASSOC;
56
57
    /**
58
     * 字段属性大小写
59
     * @var int
60
     */
61
    protected $attrCase = PDO::CASE_LOWER;
62
63
    /**
64
     * 数据表信息
65
     * @var array
66
     */
67
    protected $info = [];
68
69
    /**
70
     * 查询开始时间
71
     * @var float
72
     */
73
    protected $queryStartTime;
74
75
    /**
76
     * PDO连接参数
77
     * @var array
78
     */
79
    protected $params = [
80
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
81
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
82
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
83
        PDO::ATTR_STRINGIFY_FETCHES => false,
84
        PDO::ATTR_EMULATE_PREPARES  => false,
85
    ];
86
87
    /**
88
     * 参数绑定类型映射
89
     * @var array
90
     */
91
    protected $bindType = [
92
        'string'  => PDO::PARAM_STR,
93
        'str'     => PDO::PARAM_STR,
94
        'integer' => PDO::PARAM_INT,
95
        'int'     => PDO::PARAM_INT,
96
        'boolean' => PDO::PARAM_BOOL,
97
        'bool'    => PDO::PARAM_BOOL,
98
        'float'   => self::PARAM_FLOAT,
99
    ];
100
101
    /**
102
     * 服务器断线标识字符
103
     * @var array
104
     */
105
    protected $breakMatchStr = [
106
        'server has gone away',
107
        'no connection to the server',
108
        'Lost connection',
109
        'is dead or not enabled',
110
        'Error while sending',
111
        'decryption failed or bad record mac',
112
        'server closed the connection unexpectedly',
113
        'SSL connection has been closed unexpectedly',
114
        'Error writing data to the connection',
115
        'Resource deadlock avoided',
116
        'failed with errno',
117
    ];
118
119
    /**
120
     * 绑定参数
121
     * @var array
122
     */
123
    protected $bind = [];
124
125
    /**
126
     * 获取当前连接器类对应的Query类
127
     * @access public
128
     * @return string
129
     */
130
    public function getQueryClass(): string
131
    {
132
        return $this->getConfig('query') ?: Query::class;
133
    }
134
135
    /**
136
     * 获取当前连接器类对应的Builder类
137
     * @access public
138
     * @return string
139
     */
140
    public function getBuilderClass(): string
141
    {
142
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
143
    }
144
145
    /**
146
     * 解析pdo连接的dsn信息
147
     * @access protected
148
     * @param array $config 连接信息
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
149
     * @return string
150
     */
151
    abstract protected function parseDsn(array $config);
152
153
    /**
154
     * 取得数据表的字段信息
155
     * @access public
156
     * @param string $tableName 数据表名称
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
157
     * @return array
158
     */
159
    abstract public function getFields(string $tableName);
160
161
    /**
162
     * 取得数据库的表信息
163
     * @access public
164
     * @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...
165
     * @return array
166
     */
167
    abstract public function getTables(string $dbName);
168
169
    /**
170
     * SQL性能分析
171
     * @access protected
172
     * @param string $sql SQL语句
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
173
     * @return array
174
     */
175
    abstract protected function getExplain(string $sql);
176
177
    /**
178
     * 对返数据表字段信息进行大小写转换出来
179
     * @access public
180
     * @param array $info 字段信息
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
181
     * @return array
182
     */
183
    public function fieldCase(array $info): array
184
    {
185
        // 字段大小写转换
186
        switch ($this->attrCase) {
187
            case PDO::CASE_LOWER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
188
                $info = array_change_key_case($info);
189
                break;
190
            case PDO::CASE_UPPER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
191
                $info = array_change_key_case($info, CASE_UPPER);
192
                break;
193
            case PDO::CASE_NATURAL:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
194
            default:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
195
                // 不做转换
196
        }
197
198
        return $info;
199
    }
200
201
    /**
202
     * 获取字段绑定类型
203
     * @access public
204
     * @param string $type 字段类型
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
205
     * @return integer
206
     */
207
    public function getFieldBindType(string $type): int
208
    {
209
        if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
210
            $bind = $this->bindType[$type];
211
        } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
212
            $bind = PDO::PARAM_STR;
213
        } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
214
            $bind = self::PARAM_FLOAT;
215
        } elseif (preg_match('/(int|serial|bit)/is', $type)) {
216
            $bind = PDO::PARAM_INT;
217
        } elseif (preg_match('/bool/is', $type)) {
218
            $bind = PDO::PARAM_BOOL;
219
        } else {
220
            $bind = PDO::PARAM_STR;
221
        }
222
223
        return $bind;
224
    }
225
226
    /**
227
     * 获取数据表信息
228
     * @access public
229
     * @param mixed  $tableName 数据表名 留空自动获取
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
230
     * @param string $fetch     获取信息类型 包括 fields type bind pk
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
231
     * @return mixed
232
     */
233
    public function getTableInfo($tableName, string $fetch = '')
234
    {
235
        if (is_array($tableName)) {
236
            $tableName = key($tableName) ?: current($tableName);
237
        }
238
239
        if (strpos($tableName, ',')) {
240
            // 多表不获取字段信息
241
            return [];
242
        }
243
244
        // 修正子查询作为表名的问题
245
        if (strpos($tableName, ')')) {
246
            return [];
247
        }
248
249
        list($tableName) = explode(' ', $tableName);
250
251
        if (!strpos($tableName, '.')) {
252
            $schema = $this->getConfig('database') . '.' . $tableName;
253
        } else {
254
            $schema = $tableName;
255
        }
256
257
        if (!isset($this->info[$schema])) {
258
            // 读取缓存
259
            $cacheFile = Container::pull('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php';
260
261
            if (!$this->config['debug'] && is_file($cacheFile)) {
262
                $info = include $cacheFile;
263
            } else {
264
                $info = $this->getFields($tableName);
265
            }
266
267
            $fields = array_keys($info);
268
            $bind   = $type   = [];
269
270
            foreach ($info as $key => $val) {
271
                // 记录字段类型
272
                $type[$key] = $val['type'];
273
                $bind[$key] = $this->getFieldBindType($val['type']);
274
275
                if (!empty($val['primary'])) {
276
                    $pk[] = $key;
277
                }
278
            }
279
280
            if (isset($pk)) {
281
                // 设置主键
282
                $pk = count($pk) > 1 ? $pk : $pk[0];
283
            } else {
284
                $pk = null;
285
            }
286
287
            $this->info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
288
        }
289
290
        return $fetch ? $this->info[$schema][$fetch] : $this->info[$schema];
291
    }
292
293
    /**
294
     * 获取数据表的主键
295
     * @access public
296
     * @param mixed $tableName 数据表名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
297
     * @return string|array
298
     */
299
    public function getPk($tableName)
300
    {
301
        return $this->getTableInfo($tableName, 'pk');
302
    }
303
304
    /**
305
     * 获取数据表字段信息
306
     * @access public
307
     * @param mixed $tableName 数据表名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
308
     * @return array
309
     */
310
    public function getTableFields($tableName): array
311
    {
312
        return $this->getTableInfo($tableName, 'fields');
313
    }
314
315
    /**
316
     * 获取数据表字段类型
317
     * @access public
318
     * @param mixed  $tableName 数据表名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
319
     * @param string $field     字段名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
320
     * @return array|string
321
     */
322
    public function getFieldsType($tableName, string $field = null)
323
    {
324
        $result = $this->getTableInfo($tableName, 'type');
325
326
        if ($field && isset($result[$field])) {
327
            return $result[$field];
328
        }
329
330
        return $result;
331
    }
332
333
    /**
334
     * 获取数据表绑定信息
335
     * @access public
336
     * @param mixed $tableName 数据表名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
337
     * @return array
338
     */
339
    public function getFieldsBind($tableName): array
340
    {
341
        return $this->getTableInfo($tableName, 'bind');
342
    }
343
344
    /**
345
     * 连接数据库方法
346
     * @access public
347
     * @param array      $config         连接参数
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
348
     * @param integer    $linkNum        连接序号
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
349
     * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式)
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
350
     * @return PDO
351
     * @throws Exception
352
     */
353
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
354
    {
355
        if (isset($this->links[$linkNum])) {
356
            return $this->links[$linkNum];
357
        }
358
359
        if (empty($config)) {
360
            $config = $this->config;
361
        } else {
362
            $config = array_merge($this->config, $config);
363
        }
364
365
        // 连接参数
366
        if (isset($config['params']) && is_array($config['params'])) {
367
            $params = $config['params'] + $this->params;
368
        } else {
369
            $params = $this->params;
370
        }
371
372
        // 记录当前字段属性大小写设置
373
        $this->attrCase = $params[PDO::ATTR_CASE];
374
375
        if (!empty($config['break_match_str'])) {
376
            $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
377
        }
378
379
        try {
380
            if (empty($config['dsn'])) {
381
                $config['dsn'] = $this->parseDsn($config);
382
            }
383
384
            $startTime             = microtime(true);
385
            $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
386
            // 记录数据库连接信息
387
            $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
388
389
            return $this->links[$linkNum];
390
        } catch (\PDOException $e) {
391
            if ($autoConnection) {
392
                $this->log->error($e->getMessage());
393
                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\PDOConnection::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

393
                return $this->connect(/** @scrutinizer ignore-type */ $autoConnection, $linkNum);
Loading history...
394
            } else {
395
                throw $e;
396
            }
397
        }
398
    }
399
400
    /**
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...
401
     * 创建PDO实例
402
     * @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...
403
     * @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...
404
     * @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...
405
     * @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...
406
     * @return PDO
407
     */
408
    protected function createPdo($dsn, $username, $password, $params)
409
    {
410
        return new PDO($dsn, $username, $password, $params);
411
    }
412
413
    /**
414
     * 释放查询结果
415
     * @access public
416
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
417
    public function free(): void
418
    {
419
        $this->PDOStatement = null;
420
    }
421
422
    /**
423
     * 获取PDO对象
424
     * @access public
425
     * @return \PDO|false
426
     */
427
    public function getPdo()
428
    {
429
        if (!$this->linkID) {
430
            return false;
431
        }
432
433
        return $this->linkID;
434
    }
435
436
    /**
437
     * 执行查询 使用生成器返回数据
438
     * @access public
439
     * @param Query        $query     查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
440
     * @param string       $sql       sql指令
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
441
     * @param array        $bind      参数绑定
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
442
     * @param \think\Model $model     模型对象实例
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
443
     * @param array        $condition 查询条件
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
444
     * @return \Generator
445
     */
446
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
447
    {
448
        $this->queryPDOStatement($query, $sql, $bind);
449
450
        // 返回结果集
451
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
452
            if ($model) {
453
                yield $model->newInstance($result, $condition)->setQuery($query);
454
            } else {
455
                yield $result;
456
            }
457
        }
458
    }
459
460
    /**
461
     * 执行查询 返回数据集
462
     * @access public
463
     * @param Query  $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
464
     * @param mixed  $sql   sql指令
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
465
     * @param array  $bind  参数绑定
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
466
     * @return array
467
     * @throws BindParamException
468
     * @throws \PDOException
469
     * @throws \Exception
470
     * @throws \Throwable
471
     */
472
    public function query(Query $query, $sql, array $bind = []): array
473
    {
474
        // 分析查询表达式
475
        $query->parseOptions();
476
477
        if ($query->getOptions('cache')) {
478
            // 检查查询缓存
479
            $cacheItem = $this->parseCache($query, $query->getOptions('cache'));
0 ignored issues
show
Bug introduced by
It seems like $query->getOptions('cache') can also be of type null; however, parameter $cache of think\db\PDOConnection::parseCache() 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

479
            $cacheItem = $this->parseCache($query, /** @scrutinizer ignore-type */ $query->getOptions('cache'));
Loading history...
480
            $resultSet = $this->cache->get($cacheItem->getKey());
481
482
            if (false !== $resultSet) {
483
                return $resultSet;
484
            }
485
        }
486
487
        if ($sql instanceof \Closure) {
488
            $sql  = $sql($query);
489
            $bind = $query->getBind();
490
        }
491
492
        $master    = $query->getOptions('master') ? true : false;
493
        $procedure = $query->getOptions('procedure') ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
494
495
        $this->getPDOStatement($sql, $bind, $master, $procedure);
496
497
        $resultSet = $this->getResult($procedure);
498
499
        if (isset($cacheItem) && $resultSet) {
500
            // 缓存数据集
501
            $cacheItem->set($resultSet);
502
            $this->cacheData($cacheItem);
503
        }
504
505
        return $resultSet;
506
    }
507
508
    /**
509
     * 执行查询但只返回PDOStatement对象
510
     * @access public
511
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
512
     * @return \PDOStatement
513
     */
514
    public function pdo(Query $query): PDOStatement
515
    {
516
        $bind = $query->getBind();
517
        // 生成查询SQL
518
        $sql = $this->builder->select($query);
519
520
        return $this->queryPDOStatement($query, $sql, $bind);
521
    }
522
523
    /**
524
     * 执行查询但只返回PDOStatement对象
525
     * @access public
526
     * @param string $sql       sql指令
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
527
     * @param array  $bind      参数绑定
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
528
     * @param bool   $master    是否在主服务器读操作
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
529
     * @param bool   $procedure 是否为存储过程调用
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
530
     * @return PDOStatement
531
     * @throws BindParamException
532
     * @throws \PDOException
533
     * @throws \Exception
534
     * @throws \Throwable
535
     */
536
    public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
537
    {
538
        $this->initConnect($this->readMaster ?: $master);
539
540
        // 记录SQL语句
541
        $this->queryStr = $sql;
542
543
        $this->bind = $bind;
544
545
        $this->db->updateQueryTimes();
546
547
        try {
548
            // 调试开始
549
            $this->debug(true);
550
551
            // 预处理
552
            $this->PDOStatement = $this->linkID->prepare($sql);
553
554
            // 参数绑定
555
            if ($procedure) {
556
                $this->bindParam($bind);
557
            } else {
558
                $this->bindValue($bind);
559
            }
560
561
            // 执行查询
562
            $this->PDOStatement->execute();
563
564
            // 调试结束
565
            $this->debug(false, '', $master);
566
567
            return $this->PDOStatement;
568
        } catch (\Throwable | \Exception $e) {
569
            if ($this->isBreak($e)) {
570
                return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
571
            }
572
573
            if ($e instanceof \PDOException) {
574
                throw new PDOException($e, $this->config, $this->getLastsql());
575
            } else {
576
                throw $e;
577
            }
578
        }
579
    }
580
581
    /**
582
     * 执行语句
583
     * @access public
584
     * @param Query  $query  查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
585
     * @param string $sql    sql指令
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
586
     * @param array  $bind   参数绑定
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
587
     * @param bool   $origin 是否原生查询
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
588
     * @return int
589
     * @throws BindParamException
590
     * @throws \PDOException
591
     * @throws \Exception
592
     * @throws \Throwable
593
     */
594
    public function execute(Query $query, string $sql, array $bind = [], bool $origin = false): int
595
    {
596
        if ($origin) {
597
            $query->parseOptions();
598
        }
599
600
        $this->queryPDOStatement($query->master(true), $sql, $bind);
601
602
        if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
603
            $this->readMaster = true;
604
        }
605
606
        $this->numRows = $this->PDOStatement->rowCount();
607
608
        if ($query->getOptions('cache')) {
609
            // 清理缓存数据
610
            $cacheItem = $this->parseCache($query, $query->getOptions('cache'), $sql);
0 ignored issues
show
Unused Code introduced by
The call to think\db\PDOConnection::parseCache() has too many arguments starting with $sql. ( Ignorable by Annotation )

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

610
            /** @scrutinizer ignore-call */ 
611
            $cacheItem = $this->parseCache($query, $query->getOptions('cache'), $sql);

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...
Bug introduced by
It seems like $query->getOptions('cache') can also be of type null; however, parameter $cache of think\db\PDOConnection::parseCache() 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

610
            $cacheItem = $this->parseCache($query, /** @scrutinizer ignore-type */ $query->getOptions('cache'), $sql);
Loading history...
611
            $key       = $cacheItem->getKey();
612
            $tag       = $cacheItem->getTag();
613
614
            if (isset($key) && $this->cache->get($key)) {
615
                $this->cache->delete($key);
616
            } elseif (!empty($tag)) {
617
                $this->cache->tag($tag)->clear();
618
            }
619
        }
620
621
        return $this->numRows;
622
    }
623
624
    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...
625
    {
626
        $options   = $query->getOptions();
627
        $master    = !empty($options['master']) ? true : false;
628
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
629
630
        return $this->getPDOStatement($sql, $bind, $master, $procedure);
631
    }
632
633
    /**
634
     * 查找单条记录
635
     * @access public
636
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
637
     * @return array
638
     * @throws DbException
639
     * @throws ModelNotFoundException
640
     * @throws DataNotFoundException
641
     */
642
    public function find(Query $query): array
643
    {
644
        // 事件回调
645
        $result = $this->db->trigger('before_find', $query);
646
647
        if (!$result) {
648
            // 执行查询
649
            $resultSet = $this->query($query, function ($query) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
650
                return $this->builder->select($query, true);
651
            });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
652
653
            $result = $resultSet[0] ?? [];
654
        }
655
656
        return $result;
657
    }
658
659
    /**
660
     * 使用游标查询记录
661
     * @access public
662
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
663
     * @return \Generator
664
     */
665
    public function cursor(Query $query)
666
    {
667
        // 分析查询表达式
668
        $options = $query->parseOptions();
669
670
        // 生成查询SQL
671
        $sql = $this->builder->select($query);
672
673
        $condition = $options['where']['AND'] ?? null;
674
675
        // 执行查询操作
676
        return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
677
    }
678
679
    /**
680
     * 查找记录
681
     * @access public
682
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
683
     * @return array
684
     * @throws DbException
685
     * @throws ModelNotFoundException
686
     * @throws DataNotFoundException
687
     */
688
    public function select(Query $query): array
689
    {
690
        $resultSet = $this->db->trigger('before_select', $query);
691
692
        if (!$resultSet) {
693
            // 执行查询操作
694
            $resultSet = $this->query($query, function ($query) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
695
                return $this->builder->select($query);
696
            });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
697
        }
698
699
        return $resultSet;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $resultSet could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
700
    }
701
702
    /**
703
     * 插入记录
704
     * @access public
705
     * @param Query   $query        查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
706
     * @param boolean $getLastInsID 返回自增主键
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
707
     * @return mixed
708
     */
709
    public function insert(Query $query, bool $getLastInsID = false)
710
    {
711
        // 分析查询表达式
712
        $options = $query->parseOptions();
713
714
        // 生成SQL语句
715
        $sql = $this->builder->insert($query);
716
717
        // 执行操作
718
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
719
720
        if ($result) {
721
            $sequence  = $options['sequence'] ?? null;
722
            $lastInsId = $query->getLastInsID($sequence);
723
724
            $data = $options['data'];
725
726
            if ($lastInsId) {
727
                $pk = $query->getPk();
728
                if (is_string($pk)) {
729
                    $data[$pk] = $lastInsId;
730
                }
731
            }
732
733
            $query->setOption('data', $data);
734
735
            $this->db->trigger('after_insert', $query);
736
737
            if ($getLastInsID) {
738
                return $lastInsId;
739
            }
740
        }
741
742
        return $result;
743
    }
744
745
    /**
746
     * 批量插入记录
747
     * @access public
748
     * @param Query   $query   查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
749
     * @param mixed   $dataSet 数据集
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
750
     * @param integer $limit   每次写入数据限制
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
751
     * @return integer
752
     * @throws \Exception
753
     * @throws \Throwable
754
     */
755
    public function insertAll(Query $query, array $dataSet = [], int $limit = 0): int
756
    {
757
        if (!is_array(reset($dataSet))) {
758
            return 0;
759
        }
760
761
        $query->parseOptions();
762
763
        if ($limit) {
764
            // 分批写入 自动启动事务支持
765
            $this->startTrans();
766
767
            try {
768
                $array = array_chunk($dataSet, $limit, true);
769
                $count = 0;
770
771
                foreach ($array as $item) {
772
                    $sql = $this->builder->insertAll($query, $item);
773
                    $count += $this->execute($query, $sql, $query->getBind());
774
                }
775
776
                // 提交事务
777
                $this->commit();
778
            } catch (\Exception | \Throwable $e) {
779
                $this->rollback();
780
                throw $e;
781
            }
782
783
            return $count;
784
        }
785
786
        $sql = $this->builder->insertAll($query, $dataSet);
787
788
        return $this->execute($query, $sql, $query->getBind());
789
    }
790
791
    /**
792
     * 通过Select方式插入记录
793
     * @access public
794
     * @param Query  $query  查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
795
     * @param array  $fields 要插入的数据表字段名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
796
     * @param string $table  要插入的数据表名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
797
     * @return integer
798
     * @throws PDOException
799
     */
800
    public function selectInsert(Query $query, array $fields, string $table): int
801
    {
802
        // 分析查询表达式
803
        $query->parseOptions();
804
805
        $sql = $this->builder->selectInsert($query, $fields, $table);
806
807
        return $this->execute($query, $sql, $query->getBind());
808
    }
809
810
    /**
811
     * 更新记录
812
     * @access public
813
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
814
     * @return integer
815
     * @throws Exception
816
     * @throws PDOException
817
     */
818
    public function update(Query $query): int
819
    {
820
        $query->parseOptions();
821
822
        // 生成UPDATE SQL语句
823
        $sql = $this->builder->update($query);
824
825
        // 执行操作
826
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
827
828
        if ($result) {
829
            $this->db->trigger('after_update', $query);
830
        }
831
832
        return $result;
833
    }
834
835
    /**
836
     * 删除记录
837
     * @access public
838
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
839
     * @return int
840
     * @throws Exception
841
     * @throws PDOException
842
     */
843
    public function delete(Query $query): int
844
    {
845
        // 分析查询表达式
846
        $query->parseOptions();
847
848
        // 生成删除SQL语句
849
        $sql = $this->builder->delete($query);
850
851
        // 执行操作
852
        $result = $this->execute($query, $sql, $query->getBind());
853
854
        if ($result) {
855
            $this->db->trigger('after_delete', $query);
856
        }
857
858
        return $result;
859
    }
860
861
    /**
862
     * 得到某个字段的值
863
     * @access public
864
     * @param Query  $query   查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
865
     * @param string $field   字段名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
866
     * @param mixed  $default 默认值
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
867
     * @param bool   $one     返回一个值
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
868
     * @return mixed
869
     */
870
    public function value(Query $query, string $field, $default = null, bool $one = true)
871
    {
872
        $options = $query->parseOptions();
873
874
        if (isset($options['field'])) {
875
            $query->removeOption('field');
876
        }
877
878
        $query->setOption('field', (array) $field);
879
880
        if (!empty($options['cache'])) {
881
            $cacheItem = $this->parseCache($query, $options['cache']);
882
            $result    = $this->cache->get($cacheItem->getKey());
883
884
            if (false !== $result) {
885
                return $result;
886
            }
887
        }
888
889
        // 生成查询SQL
890
        $sql = $this->builder->select($query, $one);
891
892
        if (isset($options['field'])) {
893
            $query->setOption('field', $options['field']);
894
        } else {
895
            $query->removeOption('field');
896
        }
897
898
        // 执行查询操作
899
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
900
901
        $result = $pdo->fetchColumn();
902
903
        if (isset($cacheItem) && false !== $result) {
904
            // 缓存数据
905
            $cacheItem->set($result);
906
            $this->cacheData($cacheItem);
907
        }
908
909
        return false !== $result ? $result : $default;
910
    }
911
912
    /**
913
     * 得到某个字段的值
914
     * @access public
915
     * @param Query  $query     查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
916
     * @param string $aggregate 聚合方法
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
917
     * @param mixed  $field     字段名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
918
     * @param bool   $force     强制转为数字类型
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
919
     * @return mixed
920
     */
921
    public function aggregate(Query $query, string $aggregate, $field, bool $force = false)
922
    {
923
        if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
924
            list($distinct, $field) = explode(' ', $field);
925
        }
926
927
        $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
928
929
        $result = $this->value($query, $field, 0, false);
930
931
        return $force ? (float) $result : $result;
932
    }
933
934
    /**
935
     * 得到某个列的数组
936
     * @access public
937
     * @param Query  $query  查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
938
     * @param string $column 字段名 多个字段用逗号分隔
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
939
     * @param string $key    索引
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
940
     * @return array
941
     */
942
    public function column(Query $query, string $column, string $key = ''): array
943
    {
944
        $options = $query->parseOptions();
945
946
        if (isset($options['field'])) {
947
            $query->removeOption('field');
948
        }
949
950
        if ($key && '*' != $column) {
951
            $field = $key . ',' . $column;
952
        } else {
953
            $field = $column;
954
        }
955
956
        $field = array_map('trim', explode(',', $field));
957
958
        $query->setOption('field', $field);
959
960
        if (!empty($options['cache'])) {
961
            // 判断查询缓存
962
            $cacheItem = $this->parseCache($query, $options['cache']);
963
            $result    = $this->cache->get($cacheItem->getKey());
964
965
            if (false !== $result) {
966
                return $result;
967
            }
968
        }
969
970
        // 生成查询SQL
971
        $sql = $this->builder->select($query);
972
973
        if (isset($options['field'])) {
974
            $query->setOption('field', $options['field']);
975
        } else {
976
            $query->removeOption('field');
977
        }
978
979
        // 执行查询操作
980
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
981
982
        $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
983
984
        if (empty($resultSet)) {
985
            $result = [];
986
        } elseif (('*' == $column || strpos($column, ',')) && $key) {
987
            $result = array_column($resultSet, null, $key);
988
        } else {
989
            $fields = array_keys($resultSet[0]);
990
            $key    = $key ?: array_shift($fields);
991
992
            if (strpos($key, '.')) {
993
                list($alias, $key) = explode('.', $key);
994
            }
995
996
            $result = array_column($resultSet, $column, $key);
997
        }
998
999
        if (isset($cacheItem)) {
1000
            // 缓存数据
1001
            $cacheItem->set($result);
1002
            $this->cacheData($cacheItem);
1003
        }
1004
1005
        return $result;
1006
    }
1007
1008
    /**
1009
     * 根据参数绑定组装最终的SQL语句 便于调试
1010
     * @access public
1011
     * @param string $sql  带参数绑定的sql语句
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1012
     * @param array  $bind 参数绑定列表
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1013
     * @return string
1014
     */
1015
    public function getRealSql(string $sql, array $bind = []): string
1016
    {
1017
        foreach ($bind as $key => $val) {
1018
            $value = is_array($val) ? $val[0] : $val;
1019
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
1020
1021
            if ((self::PARAM_FLOAT == $type || PDO::PARAM_STR == $type) && is_string($value)) {
1022
                $value = '\'' . addslashes($value) . '\'';
1023
            } elseif (PDO::PARAM_INT == $type && '' === $value) {
1024
                $value = 0;
1025
            }
1026
1027
            // 判断占位符
1028
            $sql = is_numeric($key) ?
1029
            substr_replace($sql, $value, strpos($sql, '?'), 1) :
1030
            substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
1031
        }
1032
1033
        return rtrim($sql);
1034
    }
1035
1036
    /**
1037
     * 参数绑定
1038
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
1039
     * 或者 ['value',123] 对应问号占位符
1040
     * @access public
1041
     * @param array $bind 要绑定的参数列表
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1042
     * @return void
1043
     * @throws BindParamException
1044
     */
1045
    protected function bindValue(array $bind = []): void
1046
    {
1047
        foreach ($bind as $key => $val) {
1048
            // 占位符
1049
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1050
1051
            if (is_array($val)) {
1052
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
1053
                    $val[0] = 0;
1054
                } elseif (self::PARAM_FLOAT == $val[1]) {
1055
                    $val[0] = is_string($val[0]) ? (float) $val[0] : $val[0];
1056
                    $val[1] = PDO::PARAM_STR;
1057
                }
1058
1059
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
1060
            } else {
1061
                $result = $this->PDOStatement->bindValue($param, $val);
1062
            }
1063
1064
            if (!$result) {
1065
                throw new BindParamException(
1066
                    "Error occurred  when binding parameters '{$param}'",
1067
                    $this->config,
1068
                    $this->getLastsql(),
1069
                    $bind
1070
                );
1071
            }
1072
        }
1073
    }
1074
1075
    /**
1076
     * 存储过程的输入输出参数绑定
1077
     * @access public
1078
     * @param array $bind 要绑定的参数列表
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1079
     * @return void
1080
     * @throws BindParamException
1081
     */
1082
    protected function bindParam(array $bind): void
1083
    {
1084
        foreach ($bind as $key => $val) {
1085
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1086
1087
            if (is_array($val)) {
1088
                array_unshift($val, $param);
1089
                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
1090
            } else {
1091
                $result = $this->PDOStatement->bindValue($param, $val);
1092
            }
1093
1094
            if (!$result) {
1095
                $param = array_shift($val);
1096
1097
                throw new BindParamException(
1098
                    "Error occurred  when binding parameters '{$param}'",
1099
                    $this->config,
1100
                    $this->getLastsql(),
1101
                    $bind
1102
                );
1103
            }
1104
        }
1105
    }
1106
1107
    /**
1108
     * 获得数据集数组
1109
     * @access protected
1110
     * @param bool $procedure 是否存储过程
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1111
     * @return array
1112
     */
1113
    protected function getResult(bool $procedure = false): array
1114
    {
1115
        if ($procedure) {
1116
            // 存储过程返回结果
1117
            return $this->procedure();
1118
        }
1119
1120
        $result = $this->PDOStatement->fetchAll($this->fetchType);
1121
1122
        $this->numRows = count($result);
1123
1124
        return $result;
1125
    }
1126
1127
    /**
1128
     * 获得存储过程数据集
1129
     * @access protected
1130
     * @return array
1131
     */
1132
    protected function procedure(): array
1133
    {
1134
        $item = [];
1135
1136
        do {
1137
            $result = $this->getResult();
1138
            if (!empty($result)) {
1139
                $item[] = $result;
1140
            }
1141
        } while ($this->PDOStatement->nextRowset());
1142
1143
        $this->numRows = count($item);
1144
1145
        return $item;
1146
    }
1147
1148
    /**
1149
     * 执行数据库事务
1150
     * @access public
1151
     * @param  callable $callback 数据操作方法回调
1152
     * @return mixed
1153
     * @throws PDOException
1154
     * @throws \Exception
1155
     * @throws \Throwable
1156
     */
1157
    public function transaction(callable $callback)
1158
    {
1159
        $this->startTrans();
1160
1161
        try {
1162
            $result = null;
1163
            if (is_callable($callback)) {
1164
                $result = $callback($this);
1165
            }
1166
1167
            $this->commit();
1168
            return $result;
1169
        } catch (\Exception | \Throwable $e) {
1170
            $this->rollback();
1171
            throw $e;
1172
        }
1173
    }
1174
1175
    /**
1176
     * 启动事务
1177
     * @access public
1178
     * @return void
1179
     * @throws \PDOException
1180
     * @throws \Exception
1181
     */
1182
    public function startTrans(): void
1183
    {
1184
        $this->initConnect(true);
1185
1186
        ++$this->transTimes;
1187
1188
        try {
1189
            if (1 == $this->transTimes) {
1190
                $this->linkID->beginTransaction();
1191
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1192
                $this->linkID->exec(
1193
                    $this->parseSavepoint('trans' . $this->transTimes)
1194
                );
1195
            }
1196
        } catch (\Exception $e) {
1197
            if ($this->isBreak($e)) {
1198
                --$this->transTimes;
1199
                $this->close()->startTrans();
1200
            }
1201
            throw $e;
1202
        }
1203
    }
1204
1205
    /**
1206
     * 用于非自动提交状态下面的查询提交
1207
     * @access public
1208
     * @return void
1209
     * @throws PDOException
1210
     */
1211
    public function commit(): void
1212
    {
1213
        $this->initConnect(true);
1214
1215
        if (1 == $this->transTimes) {
1216
            $this->linkID->commit();
1217
        }
1218
1219
        --$this->transTimes;
1220
    }
1221
1222
    /**
1223
     * 事务回滚
1224
     * @access public
1225
     * @return void
1226
     * @throws PDOException
1227
     */
1228
    public function rollback(): void
1229
    {
1230
        $this->initConnect(true);
1231
1232
        if (1 == $this->transTimes) {
1233
            $this->linkID->rollBack();
1234
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1235
            $this->linkID->exec(
1236
                $this->parseSavepointRollBack('trans' . $this->transTimes)
1237
            );
1238
        }
1239
1240
        $this->transTimes = max(0, $this->transTimes - 1);
1241
    }
1242
1243
    /**
1244
     * 是否支持事务嵌套
1245
     * @return bool
1246
     */
1247
    protected function supportSavepoint(): bool
1248
    {
1249
        return false;
1250
    }
1251
1252
    /**
1253
     * 生成定义保存点的SQL
1254
     * @access protected
1255
     * @param string $name 标识
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1256
     * @return string
1257
     */
1258
    protected function parseSavepoint(string $name): string
1259
    {
1260
        return 'SAVEPOINT ' . $name;
1261
    }
1262
1263
    /**
1264
     * 生成回滚到保存点的SQL
1265
     * @access protected
1266
     * @param string $name 标识
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1267
     * @return string
1268
     */
1269
    protected function parseSavepointRollBack(string $name): string
1270
    {
1271
        return 'ROLLBACK TO SAVEPOINT ' . $name;
1272
    }
1273
1274
    /**
1275
     * 批处理执行SQL语句
1276
     * 批处理的指令都认为是execute操作
1277
     * @access public
1278
     * @param Query $query    查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1279
     * @param array $sqlArray SQL批处理指令
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1280
     * @param array $bind     参数绑定
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1281
     * @return bool
1282
     */
1283
    public function batchQuery(Query $query, array $sqlArray = [], array $bind = []): bool
1284
    {
1285
        // 自动启动事务支持
1286
        $this->startTrans();
1287
1288
        try {
1289
            foreach ($sqlArray as $sql) {
1290
                $this->execute($query, $sql, $bind);
1291
            }
1292
            // 提交事务
1293
            $this->commit();
1294
        } catch (\Exception $e) {
1295
            $this->rollback();
1296
            throw $e;
1297
        }
1298
1299
        return true;
1300
    }
1301
1302
    /**
1303
     * 关闭数据库(或者重新连接)
1304
     * @access public
1305
     * @return $this
1306
     */
1307
    public function close()
1308
    {
1309
        $this->linkID    = null;
1310
        $this->linkWrite = null;
1311
        $this->linkRead  = null;
1312
        $this->links     = [];
1313
1314
        $this->free();
1315
1316
        return $this;
1317
    }
1318
1319
    /**
1320
     * 是否断线
1321
     * @access protected
1322
     * @param \PDOException|\Exception $e 异常对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1323
     * @return bool
1324
     */
1325
    protected function isBreak($e): bool
1326
    {
1327
        if (!$this->config['break_reconnect']) {
1328
            return false;
1329
        }
1330
1331
        $error = $e->getMessage();
1332
1333
        foreach ($this->breakMatchStr as $msg) {
1334
            if (false !== stripos($error, $msg)) {
1335
                return true;
1336
            }
1337
        }
1338
1339
        return false;
1340
    }
1341
1342
    /**
1343
     * 获取最近一次查询的sql语句
1344
     * @access public
1345
     * @return string
1346
     */
1347
    public function getLastSql(): string
1348
    {
1349
        return $this->getRealSql($this->queryStr, $this->bind);
1350
    }
1351
1352
    /**
1353
     * 获取最近插入的ID
1354
     * @access public
1355
     * @param string $sequence 自增序列名
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1356
     * @return string
1357
     */
1358
    public function getLastInsID(string $sequence = null): string
1359
    {
1360
        return $this->linkID->lastInsertId($sequence);
1361
    }
1362
1363
    /**
1364
     * 获取返回或者影响的记录数
1365
     * @access public
1366
     * @return integer
1367
     */
1368
    public function getNumRows(): int
1369
    {
1370
        return $this->numRows;
1371
    }
1372
1373
    /**
1374
     * 获取最近的错误信息
1375
     * @access public
1376
     * @return string
1377
     */
1378
    public function getError(): string
1379
    {
1380
        if ($this->PDOStatement) {
1381
            $error = $this->PDOStatement->errorInfo();
1382
            $error = $error[1] . ':' . $error[2];
1383
        } else {
1384
            $error = '';
1385
        }
1386
1387
        if ('' != $this->queryStr) {
1388
            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
1389
        }
1390
1391
        return $error;
1392
    }
1393
1394
    /**
1395
     * 数据库调试 记录当前SQL及分析性能
1396
     * @access protected
1397
     * @param boolean $start  调试开始标记 true 开始 false 结束
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1398
     * @param string  $sql    执行的SQL语句 留空自动获取
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1399
     * @param bool    $master 主从标记
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1400
     * @return void
1401
     */
1402
    protected function debug(bool $start, string $sql = '', bool $master = false): void
1403
    {
1404
        if (!empty($this->config['debug'])) {
1405
            // 开启数据库调试模式
1406
            if ($start) {
1407
                $this->queryStartTime = microtime(true);
1408
            } else {
1409
                // 记录操作结束时间
1410
                $runtime = number_format((microtime(true) - $this->queryStartTime), 6);
1411
                $sql     = $sql ?: $this->getLastsql();
1412
                $result  = [];
1413
1414
                // SQL性能分析
1415
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
1416
                    $result = $this->getExplain($sql);
1417
                }
1418
1419
                // SQL监听
1420
                $this->triggerSql($sql, $runtime, $result, $master);
1421
            }
1422
        }
1423
    }
1424
1425
    /**
1426
     * 触发SQL事件
1427
     * @access protected
1428
     * @param string $sql     SQL语句
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1429
     * @param string $runtime SQL运行时间
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1430
     * @param mixed  $explain SQL分析
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1431
     * @param bool   $master  主从标记
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1432
     * @return void
1433
     */
1434
    protected function triggerSql(string $sql, string $runtime, array $explain = [], bool $master = false): void
1435
    {
1436
        $listen = $this->db->getListen();
1437
        if (!empty($listen)) {
1438
            foreach ($listen as $callback) {
1439
                if (is_callable($callback)) {
1440
                    $callback($sql, $runtime, $explain, $master);
1441
                }
1442
            }
1443
        } else {
1444
            if ($this->config['deploy']) {
1445
                // 分布式记录当前操作的主从
1446
                $master = $master ? 'master|' : 'slave|';
1447
            } else {
1448
                $master = '';
1449
            }
1450
1451
            // 未注册监听则记录到日志中
1452
            $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
1453
1454
            if (!empty($explain)) {
1455
                $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
1456
            }
1457
        }
1458
    }
1459
1460
    /**
1461
     * 初始化数据库连接
1462
     * @access protected
1463
     * @param boolean $master 是否主服务器
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1464
     * @return void
1465
     */
1466
    protected function initConnect(bool $master = true): void
1467
    {
1468
        if (!empty($this->config['deploy'])) {
1469
            // 采用分布式数据库
1470
            if ($master || $this->transTimes) {
1471
                if (!$this->linkWrite) {
1472
                    $this->linkWrite = $this->multiConnect(true);
1473
                }
1474
1475
                $this->linkID = $this->linkWrite;
1476
            } else {
1477
                if (!$this->linkRead) {
1478
                    $this->linkRead = $this->multiConnect(false);
1479
                }
1480
1481
                $this->linkID = $this->linkRead;
1482
            }
1483
        } elseif (!$this->linkID) {
1484
            // 默认单数据库
1485
            $this->linkID = $this->connect();
1486
        }
1487
    }
1488
1489
    /**
1490
     * 连接分布式服务器
1491
     * @access protected
1492
     * @param boolean $master 主服务器
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1493
     * @return PDO
1494
     */
1495
    protected function multiConnect(bool $master = false): PDO
1496
    {
1497
        $config = [];
1498
1499
        // 分布式数据库配置解析
1500
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1501
            $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
1502
        }
1503
1504
        // 主服务器序号
1505
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
1506
1507
        if ($this->config['rw_separate']) {
1508
            // 主从式采用读写分离
1509
            if ($master) // 主服务器写入
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...) // 主服务器写入\n {\n"
Loading history...
1510
            {
1511
                $r = $m;
1512
            } elseif (is_numeric($this->config['slave_no'])) {
1513
                // 指定服务器读
1514
                $r = $this->config['slave_no'];
1515
            } else {
1516
                // 读操作连接从服务器 每次随机连接的数据库
1517
                $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
1518
            }
1519
        } else {
1520
            // 读写操作不区分服务器 每次随机连接的数据库
1521
            $r = floor(mt_rand(0, count($config['hostname']) - 1));
1522
        }
1523
        $dbMaster = false;
1524
1525
        if ($m != $r) {
1526
            $dbMaster = [];
1527
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1528
                $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
1529
            }
1530
        }
1531
1532
        $dbConfig = [];
1533
1534
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1535
            $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
1536
        }
1537
1538
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
1539
    }
1540
1541
    /**
1542
     * 分析缓存
1543
     * @access protected
1544
     * @param Query $query 查询对象
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1545
     * @param array $cache 缓存信息
1 ignored issue
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1546
     * @return CacheItem
1547
     */
1548
    protected function parseCache(Query $query, array $cache): CacheItem
1549
    {
1550
        list($key, $expire, $tag) = $cache;
1551
1552
        if ($key instanceof CacheItem) {
1553
            $cacheItem = $key;
1554
        } else {
1555
            if (true === $key) {
1556
                if (!empty($query->getOptions('key'))) {
1557
                    $key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
1558
                } else {
1559
                    $key = md5($this->getConfig('database') . serialize($query->getOptions()) . serialize($query->getBind(false)));
1560
                }
1561
            }
1562
1563
            $cacheItem = new CacheItem($key);
1564
            $cacheItem->expire($expire);
1565
            $cacheItem->tag($tag);
1566
        }
1567
1568
        return $cacheItem;
1569
    }
1570
1571
}
1572