Completed
Pull Request — 6.0 (#1849)
by liu
07:21 queued 04:50
created

PdoConnection::execute()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

608
            $cacheItem = $this->parseCache($query, /** @scrutinizer ignore-type */ $query->getOptions('cache'), $sql);
Loading history...
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

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