Passed
Pull Request — 5.1 (#1748)
by guanguans
09:27
created

Connection::prepareXa()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
// +----------------------------------------------------------------------
1 ignored issue
show
Coding Style introduced by
You must use "/**" style comments for a file comment
Loading history...
3
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006~2018 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
12
namespace think\db;
13
14
use InvalidArgumentException;
15
use PDO;
16
use PDOStatement;
17
use think\Container;
18
use think\Db;
19
use think\db\exception\BindParamException;
20
use think\Debug;
21
use think\Exception;
22
use think\exception\PDOException;
23
use think\Loader;
24
25
abstract class Connection
1 ignored issue
show
Coding Style introduced by
Missing class doc comment
Loading history...
26
{
27
    const PARAM_FLOAT          = 21;
28
    protected static $instance = [];
29
    /** @var PDOStatement PDO操作实例 */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
30
    protected $PDOStatement;
31
32
    /** @var string 当前SQL指令 */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
33
    protected $queryStr = '';
34
    // 返回或者影响记录数
35
    protected $numRows = 0;
36
    // 事务指令数
37
    protected $transTimes = 0;
38
    // 错误信息
39
    protected $error = '';
40
41
    /** @var PDO[] 数据库连接ID 支持多个连接 */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
42
    protected $links = [];
43
44
    /** @var PDO 当前连接ID */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
45
    protected $linkID;
46
    protected $linkRead;
47
    protected $linkWrite;
48
49
    // 查询结果类型
50
    protected $fetchType = PDO::FETCH_ASSOC;
51
    // 字段属性大小写
52
    protected $attrCase = PDO::CASE_LOWER;
53
    // 监听回调
54
    protected static $event = [];
55
56
    // 数据表信息
57
    protected static $info = [];
58
59
    // 使用Builder类
60
    protected $builderClassName;
61
    // Builder对象
62
    protected $builder;
63
    // 数据库连接参数配置
64
    protected $config = [
65
        // 数据库类型
66
        'type'            => '',
67
        // 服务器地址
68
        'hostname'        => '',
69
        // 数据库名
70
        'database'        => '',
71
        // 用户名
72
        'username'        => '',
73
        // 密码
74
        'password'        => '',
75
        // 端口
76
        'hostport'        => '',
77
        // 连接dsn
78
        'dsn'             => '',
79
        // 数据库连接参数
80
        'params'          => [],
81
        // 数据库编码默认采用utf8
82
        'charset'         => 'utf8',
83
        // 数据库表前缀
84
        'prefix'          => '',
85
        // 数据库调试模式
86
        'debug'           => false,
87
        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
88
        'deploy'          => 0,
89
        // 数据库读写是否分离 主从式有效
90
        'rw_separate'     => false,
91
        // 读写分离后 主服务器数量
92
        'master_num'      => 1,
93
        // 指定从服务器序号
94
        'slave_no'        => '',
95
        // 模型写入后自动读取主服务器
96
        'read_master'     => false,
97
        // 是否严格检查字段是否存在
98
        'fields_strict'   => true,
99
        // 数据集返回类型
100
        'resultset_type'  => '',
101
        // 自动写入时间戳字段
102
        'auto_timestamp'  => false,
103
        // 时间字段取出后的默认时间格式
104
        'datetime_format' => 'Y-m-d H:i:s',
105
        // 是否需要进行SQL性能分析
106
        'sql_explain'     => false,
107
        // Builder类
108
        'builder'         => '',
109
        // Query类
110
        'query'           => '\\think\\db\\Query',
111
        // 是否需要断线重连
112
        'break_reconnect' => false,
113
        // 断线标识字符串
114
        'break_match_str' => [],
115
    ];
116
117
    // PDO连接参数
118
    protected $params = [
119
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
120
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
121
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
122
        PDO::ATTR_STRINGIFY_FETCHES => false,
123
        PDO::ATTR_EMULATE_PREPARES  => false,
124
    ];
125
126
    // 服务器断线标识字符
127
    protected $breakMatchStr = [
128
        'server has gone away',
129
        'no connection to the server',
130
        'Lost connection',
131
        'is dead or not enabled',
132
        'Error while sending',
133
        'decryption failed or bad record mac',
134
        'server closed the connection unexpectedly',
135
        'SSL connection has been closed unexpectedly',
136
        'Error writing data to the connection',
137
        'Resource deadlock avoided',
138
        'failed with errno',
139
    ];
140
141
    // 绑定参数
142
    protected $bind = [];
143
144
    /**
145
     * 架构函数 读取数据库配置信息
146
     * @access public
147
     * @param  array $config 数据库配置数组
148
     */
149
    public function __construct(array $config = [])
150
    {
151
        if (!empty($config)) {
152
            $this->config = array_merge($this->config, $config);
153
        }
154
155
        // 创建Builder对象
156
        $class = $this->getBuilderClass();
157
158
        $this->builder = new $class($this);
159
160
        // 执行初始化操作
161
        $this->initialize();
162
    }
163
164
    /**
165
     * 初始化
166
     * @access protected
167
     * @return void
168
     */
169
    protected function initialize()
170
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
171
172
    /**
173
     * 取得数据库连接类实例
174
     * @access public
175
     * @param  mixed         $config 连接配置
176
     * @param  bool|string   $name 连接标识 true 强制重新连接
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
177
     * @return Connection
178
     * @throws Exception
179
     */
180
    public static function instance($config = [], $name = false)
181
    {
182
        if (false === $name) {
183
            $name = md5(serialize($config));
184
        }
185
186
        if (true === $name || !isset(self::$instance[$name])) {
187
            if (empty($config['type'])) {
188
                throw new InvalidArgumentException('Undefined db type');
189
            }
190
191
            // 记录初始化信息
192
            Container::get('app')->log('[ DB ] INIT ' . $config['type']);
193
194
            if (true === $name) {
195
                $name = md5(serialize($config));
196
            }
197
198
            self::$instance[$name] = Loader::factory($config['type'], '\\think\\db\\connector\\', $config);
199
        }
200
201
        return self::$instance[$name];
202
    }
203
204
    /**
205
     * 获取当前连接器类对应的Builder类
206
     * @access public
207
     * @return string
208
     */
209
    public function getBuilderClass()
210
    {
211
        if (!empty($this->builderClassName)) {
212
            return $this->builderClassName;
213
        }
214
215
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
0 ignored issues
show
Bug introduced by
It seems like $this->getConfig('type') can also be of type mixed; however, parameter $str of ucfirst() does only seem to accept string, 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

215
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst(/** @scrutinizer ignore-type */ $this->getConfig('type'));
Loading history...
Bug Best Practice introduced by
The expression return $this->getConfig(...his->getConfig('type')) also could return the type mixed which is incompatible with the documented return type string.
Loading history...
216
    }
217
218
    /**
219
     * 设置当前的数据库Builder对象
220
     * @access protected
221
     * @param  Builder    $builder
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
222
     * @return void
223
     */
224
    protected function setBuilder(Builder $builder)
225
    {
226
        $this->builder = $builder;
227
228
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type think\db\Connection which is incompatible with the documented return type void.
Loading history...
229
    }
230
231
    /**
232
     * 获取当前的builder实例对象
233
     * @access public
234
     * @return Builder
235
     */
236
    public function getBuilder()
237
    {
238
        return $this->builder;
239
    }
240
241
    /**
242
     * 解析pdo连接的dsn信息
243
     * @access protected
244
     * @param  array $config 连接信息
245
     * @return string
246
     */
247
    abstract protected function parseDsn($config);
248
249
    /**
250
     * 取得数据表的字段信息
251
     * @access public
252
     * @param  string $tableName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
253
     * @return array
254
     */
255
    abstract public function getFields($tableName);
256
257
    /**
258
     * 取得数据库的表信息
259
     * @access public
260
     * @param string $dbName
1 ignored issue
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 1
Loading history...
261
     * @return array
262
     */
263
    abstract public function getTables($dbName);
264
265
    /**
266
     * SQL性能分析
267
     * @access protected
268
     * @param  string $sql
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
269
     * @return array
270
     */
271
    abstract protected function getExplain($sql);
272
273
    /**
274
     * 对返数据表字段信息进行大小写转换出来
275
     * @access public
276
     * @param  array $info 字段信息
277
     * @return array
278
     */
279
    public function fieldCase($info)
280
    {
281
        // 字段大小写转换
282
        switch ($this->attrCase) {
283
            case PDO::CASE_LOWER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
284
                $info = array_change_key_case($info);
285
                break;
286
            case PDO::CASE_UPPER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
287
                $info = array_change_key_case($info, CASE_UPPER);
288
                break;
289
            case PDO::CASE_NATURAL:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
290
            default:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
291
                // 不做转换
292
        }
293
294
        return $info;
295
    }
296
297
    /**
298
     * 获取字段绑定类型
299
     * @access public
300
     * @param  string $type 字段类型
301
     * @return integer
302
     */
303
    public function getFieldBindType($type)
304
    {
305
        if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
306
            $bind = PDO::PARAM_STR;
307
        } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
308
            $bind = self::PARAM_FLOAT;
309
        } elseif (preg_match('/(int|serial|bit)/is', $type)) {
310
            $bind = PDO::PARAM_INT;
311
        } elseif (preg_match('/bool/is', $type)) {
312
            $bind = PDO::PARAM_BOOL;
313
        } else {
314
            $bind = PDO::PARAM_STR;
315
        }
316
317
        return $bind;
318
    }
319
320
    /**
321
     * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写)
322
     * @access public
323
     * @param  string $sql sql语句
324
     * @return string
325
     */
326
    public function parseSqlTable($sql)
327
    {
328
        if (false !== strpos($sql, '__')) {
329
            $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) {
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...
330
                return $this->getConfig('prefix') . strtolower($match[1]);
0 ignored issues
show
Bug introduced by
Are you sure $this->getConfig('prefix') of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

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

330
                return /** @scrutinizer ignore-type */ $this->getConfig('prefix') . strtolower($match[1]);
Loading history...
331
            }, $sql);
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...
332
        }
333
334
        return $sql;
335
    }
336
337
    /**
338
     * 获取数据表信息
339
     * @access public
340
     * @param  mixed  $tableName 数据表名 留空自动获取
341
     * @param  string $fetch     获取信息类型 包括 fields type bind pk
342
     * @return mixed
343
     */
344
    public function getTableInfo($tableName, $fetch = '')
345
    {
346
        if (is_array($tableName)) {
347
            $tableName = key($tableName) ?: current($tableName);
348
        }
349
350
        if (strpos($tableName, ',')) {
351
            // 多表不获取字段信息
352
            return false;
353
        } else {
354
            $tableName = $this->parseSqlTable($tableName);
355
        }
356
357
        // 修正子查询作为表名的问题
358
        if (strpos($tableName, ')')) {
359
            return [];
360
        }
361
362
        list($tableName) = explode(' ', $tableName);
363
364
        if (false === strpos($tableName, '.')) {
365
            $schema = $this->getConfig('database') . '.' . $tableName;
0 ignored issues
show
Bug introduced by
Are you sure $this->getConfig('database') of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

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

365
            $schema = /** @scrutinizer ignore-type */ $this->getConfig('database') . '.' . $tableName;
Loading history...
366
        } else {
367
            $schema = $tableName;
368
        }
369
370
        if (!isset(self::$info[$schema])) {
371
            // 读取缓存
372
            $cacheFile = Container::get('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php';
373
374
            if (!$this->config['debug'] && is_file($cacheFile)) {
375
                $info = include $cacheFile;
376
            } else {
377
                $info = $this->getFields($tableName);
378
            }
379
380
            $fields = array_keys($info);
381
            $bind   = $type   = [];
382
383
            foreach ($info as $key => $val) {
384
                // 记录字段类型
385
                $type[$key] = $val['type'];
386
                $bind[$key] = $this->getFieldBindType($val['type']);
387
388
                if (!empty($val['primary'])) {
389
                    $pk[] = $key;
390
                }
391
            }
392
393
            if (isset($pk)) {
394
                // 设置主键
395
                $pk = count($pk) > 1 ? $pk : $pk[0];
396
            } else {
397
                $pk = null;
398
            }
399
400
            self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
401
        }
402
403
        return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema];
404
    }
405
406
    /**
407
     * 获取数据表的主键
408
     * @access public
409
     * @param  string $tableName 数据表名
410
     * @return string|array
411
     */
412
    public function getPk($tableName)
413
    {
414
        return $this->getTableInfo($tableName, 'pk');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getTableInfo($tableName, 'pk') could also return false which is incompatible with the documented return type array|string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
415
    }
416
417
    /**
418
     * 获取数据表字段信息
419
     * @access public
420
     * @param  string $tableName 数据表名
421
     * @return array
422
     */
423
    public function getTableFields($tableName)
424
    {
425
        return $this->getTableInfo($tableName, 'fields');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getTableInfo($tableName, 'fields') could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
426
    }
427
428
    /**
429
     * 获取数据表字段类型
430
     * @access public
431
     * @param  string $tableName 数据表名
432
     * @param  string $field    字段名
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
433
     * @return array|string
434
     */
435
    public function getFieldsType($tableName, $field = null)
436
    {
437
        $result = $this->getTableInfo($tableName, 'type');
438
439
        if ($field && isset($result[$field])) {
440
            return $result[$field];
441
        }
442
443
        return $result;
444
    }
445
446
    /**
447
     * 获取数据表绑定信息
448
     * @access public
449
     * @param  string $tableName 数据表名
450
     * @return array
451
     */
452
    public function getFieldsBind($tableName)
453
    {
454
        return $this->getTableInfo($tableName, 'bind');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getTableInfo($tableName, 'bind') could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
455
    }
456
457
    /**
458
     * 获取数据库的配置参数
459
     * @access public
460
     * @param  string $config 配置名称
461
     * @return mixed
462
     */
463
    public function getConfig($config = '')
464
    {
465
        return $config ? $this->config[$config] : $this->config;
466
    }
467
468
    /**
469
     * 设置数据库的配置参数
470
     * @access public
471
     * @param  string|array      $config 配置名称
472
     * @param  mixed             $value 配置值
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
473
     * @return void
474
     */
475
    public function setConfig($config, $value = '')
476
    {
477
        if (is_array($config)) {
478
            $this->config = array_merge($this->config, $config);
479
        } else {
480
            $this->config[$config] = $value;
481
        }
482
    }
483
484
    /**
485
     * 连接数据库方法
486
     * @access public
487
     * @param  array         $config 连接参数
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter name; 1 found
Loading history...
488
     * @param  integer       $linkNum 连接序号
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter name; 1 found
Loading history...
489
     * @param  array|bool    $autoConnection 是否自动连接主数据库(用于分布式)
490
     * @return PDO
491
     * @throws Exception
492
     */
493
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false)
494
    {
495
        if (isset($this->links[$linkNum])) {
496
            return $this->links[$linkNum];
497
        }
498
499
        if (!$config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $config of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
500
            $config = $this->config;
501
        } else {
502
            $config = array_merge($this->config, $config);
503
        }
504
505
        // 连接参数
506
        if (isset($config['params']) && is_array($config['params'])) {
507
            $params = $config['params'] + $this->params;
508
        } else {
509
            $params = $this->params;
510
        }
511
512
        // 记录当前字段属性大小写设置
513
        $this->attrCase = $params[PDO::ATTR_CASE];
514
515
        if (!empty($config['break_match_str'])) {
516
            $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
517
        }
518
519
        try {
520
            if (empty($config['dsn'])) {
521
                $config['dsn'] = $this->parseDsn($config);
522
            }
523
524
            if ($config['debug']) {
525
                $startTime = microtime(true);
526
            }
527
528
            $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
529
530
            if ($config['debug']) {
531
                // 记录数据库连接信息
532
                $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $startTime does not seem to be defined for all execution paths leading up to this point.
Loading history...
533
            }
534
535
            return $this->links[$linkNum];
536
        } catch (\PDOException $e) {
537
            if ($autoConnection) {
538
                $this->log($e->getMessage(), 'error');
539
                return $this->connect($autoConnection, $linkNum);
0 ignored issues
show
Bug introduced by
It seems like $autoConnection can also be of type true; however, parameter $config of think\db\Connection::connect() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

539
                return $this->connect(/** @scrutinizer ignore-type */ $autoConnection, $linkNum);
Loading history...
540
            } else {
541
                throw $e;
542
            }
543
        }
544
    }
545
546
    /**
547
     * 释放查询结果
548
     * @access public
549
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
550
    public function free()
551
    {
552
        $this->PDOStatement = null;
553
    }
554
555
    /**
556
     * 获取PDO对象
557
     * @access public
558
     * @return \PDO|false
559
     */
560
    public function getPdo()
561
    {
562
        if (!$this->linkID) {
563
            return false;
564
        }
565
566
        return $this->linkID;
567
    }
568
569
    /**
570
     * 执行查询 使用生成器返回数据
571
     * @access public
572
     * @param  string    $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
573
     * @param  array     $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
574
     * @param  bool      $master 是否在主服务器读操作
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
575
     * @param  Model     $model 模型对象实例
0 ignored issues
show
Bug introduced by
The type think\db\Model was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
576
     * @param  array     $condition 查询条件
577
     * @param  mixed     $relation 关联查询
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
578
     * @return \Generator
579
     */
580
    public function getCursor($sql, $bind = [], $master = false, $model = null, $condition = null, $relation = null)
581
    {
582
        $this->initConnect($master);
583
584
        // 记录SQL语句
585
        $this->queryStr = $sql;
586
587
        $this->bind = $bind;
588
589
        Db::$queryTimes++;
590
591
        // 调试开始
592
        $this->debug(true);
593
594
        // 预处理
595
        $this->PDOStatement = $this->linkID->prepare($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->linkID->prepare($sql) can also be of type boolean. However, the property $PDOStatement is declared as type PDOStatement. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
596
597
        // 是否为存储过程调用
598
        $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
599
600
        // 参数绑定
601
        if ($procedure) {
602
            $this->bindParam($bind);
603
        } else {
604
            $this->bindValue($bind);
605
        }
606
607
        // 执行查询
608
        $this->PDOStatement->execute();
609
610
        // 调试结束
611
        $this->debug(false, '', $master);
612
613
        // 返回结果集
614
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
615
            if ($model) {
616
                $instance = $model->newInstance($result, $condition);
617
618
                if ($relation) {
619
                    $instance->relationQuery($relation);
620
                }
621
622
                yield $instance;
623
            } else {
624
                yield $result;
625
            }
626
        }
627
    }
628
629
    /**
630
     * 执行查询 返回数据集
631
     * @access public
632
     * @param  string    $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
633
     * @param  array     $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
634
     * @param  bool      $master 是否在主服务器读操作
635
     * @param  bool      $pdo 是否返回PDO对象
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
636
     * @return array
637
     * @throws BindParamException
638
     * @throws \PDOException
639
     * @throws \Exception
640
     * @throws \Throwable
641
     */
642
    public function query($sql, $bind = [], $master = false, $pdo = false)
643
    {
644
        $this->initConnect($master);
645
646
        if (!$this->linkID) {
647
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
648
        }
649
650
        // 记录SQL语句
651
        $this->queryStr = $sql;
652
653
        $this->bind = $bind;
654
655
        Db::$queryTimes++;
656
657
        try {
658
            // 调试开始
659
            $this->debug(true);
660
661
            // 预处理
662
            $this->PDOStatement = $this->linkID->prepare($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->linkID->prepare($sql) can also be of type boolean. However, the property $PDOStatement is declared as type PDOStatement. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
663
664
            // 是否为存储过程调用
665
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
666
667
            // 参数绑定
668
            if ($procedure) {
669
                $this->bindParam($bind);
670
            } else {
671
                $this->bindValue($bind);
672
            }
673
674
            // 执行查询
675
            $this->PDOStatement->execute();
676
677
            // 调试结束
678
            $this->debug(false, '', $master);
679
680
            // 返回结果集
681
            return $this->getResult($pdo, $procedure);
682
        } catch (\PDOException $e) {
683
            if ($this->isBreak($e)) {
684
                return $this->close()->query($sql, $bind, $master, $pdo);
685
            }
686
687
            throw new PDOException($e, $this->config, $this->getLastsql());
688
        } catch (\Throwable $e) {
689
            if ($this->isBreak($e)) {
690
                return $this->close()->query($sql, $bind, $master, $pdo);
691
            }
692
693
            throw $e;
694
        } catch (\Exception $e) {
695
            if ($this->isBreak($e)) {
696
                return $this->close()->query($sql, $bind, $master, $pdo);
697
            }
698
699
            throw $e;
700
        }
701
    }
702
703
    /**
704
     * 执行语句
705
     * @access public
706
     * @param  string        $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
707
     * @param  array         $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
708
     * @param  Query         $query 查询对象
709
     * @return int
710
     * @throws BindParamException
711
     * @throws \PDOException
712
     * @throws \Exception
713
     * @throws \Throwable
714
     */
715
    public function execute($sql, $bind = [], Query $query = null)
716
    {
717
        $this->initConnect(true);
718
719
        if (!$this->linkID) {
720
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
721
        }
722
723
        // 记录SQL语句
724
        $this->queryStr = $sql;
725
726
        $this->bind = $bind;
727
728
        Db::$executeTimes++;
729
        try {
730
            // 调试开始
731
            $this->debug(true);
732
733
            // 预处理
734
            $this->PDOStatement = $this->linkID->prepare($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->linkID->prepare($sql) can also be of type boolean. However, the property $PDOStatement is declared as type PDOStatement. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
735
736
            // 是否为存储过程调用
737
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
738
739
            // 参数绑定
740
            if ($procedure) {
741
                $this->bindParam($bind);
742
            } else {
743
                $this->bindValue($bind);
744
            }
745
746
            // 执行语句
747
            $this->PDOStatement->execute();
748
749
            // 调试结束
750
            $this->debug(false, '', true);
751
752
            if ($query && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
753
                $query->readMaster();
754
            }
755
756
            $this->numRows = $this->PDOStatement->rowCount();
757
758
            return $this->numRows;
759
        } catch (\PDOException $e) {
760
            if ($this->isBreak($e)) {
761
                return $this->close()->execute($sql, $bind, $query);
762
            }
763
764
            throw new PDOException($e, $this->config, $this->getLastsql());
765
        } catch (\Throwable $e) {
766
            if ($this->isBreak($e)) {
767
                return $this->close()->execute($sql, $bind, $query);
768
            }
769
770
            throw $e;
771
        } catch (\Exception $e) {
772
            if ($this->isBreak($e)) {
773
                return $this->close()->execute($sql, $bind, $query);
774
            }
775
776
            throw $e;
777
        }
778
    }
779
780
    /**
781
     * 查找单条记录
782
     * @access public
783
     * @param  Query  $query        查询对象
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 8 found
Loading history...
784
     * @return array|null|\PDOStatement|string
785
     * @throws DbException
786
     * @throws ModelNotFoundException
787
     * @throws DataNotFoundException
788
     */
789
    public function find(Query $query)
790
    {
791
        // 分析查询表达式
792
        $options = $query->getOptions();
793
        $pk      = $query->getPk($options);
794
795
        $data = $options['data'];
796
        $query->setOption('limit', 1);
797
798
        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
799
            // 判断查询缓存
800
            $cache = $options['cache'];
801
802
            if (is_string($cache['key'])) {
803
                $key = $cache['key'];
804
            } else {
805
                $key = $this->getCacheKey($query, $data);
806
            }
807
808
            $result = Container::get('cache')->get($key);
809
810
            if (false !== $result) {
811
                return $result;
812
            }
813
        }
814
815
        if (is_string($pk) && !is_array($data)) {
816
            if (isset($key) && strpos($key, '|')) {
817
                list($a, $val) = explode('|', $key);
818
                $item[$pk]     = $val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$item was never initialized. Although not strictly required by PHP, it is generally a good practice to add $item = array(); before regardless.
Loading history...
819
            } else {
820
                $item[$pk] = $data;
821
            }
822
            $data = $item;
823
        }
824
825
        $query->setOption('data', $data);
826
827
        // 生成查询SQL
828
        $sql = $this->builder->select($query);
829
830
        $query->removeOption('limit');
831
832
        $bind = $query->getBind();
833
834
        if (!empty($options['fetch_sql'])) {
835
            // 获取实际执行的SQL语句
836
            return $this->getRealSql($sql, $bind);
837
        }
838
839
        // 事件回调
840
        $result = $query->trigger('before_find');
841
842
        if (!$result) {
843
            // 执行查询
844
            $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
845
846
            if ($resultSet instanceof \PDOStatement) {
0 ignored issues
show
introduced by
$resultSet is never a sub-type of PDOStatement.
Loading history...
847
                // 返回PDOStatement对象
848
                return $resultSet;
849
            }
850
851
            $result = isset($resultSet[0]) ? $resultSet[0] : null;
852
        }
853
854
        if (isset($cache) && $result) {
855
            // 缓存数据
856
            $this->cacheData($key, $result, $cache);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
857
        }
858
859
        return $result;
860
    }
861
862
    /**
863
     * 使用游标查询记录
864
     * @access public
865
     * @param  Query   $query        查询对象
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 8 found
Loading history...
866
     * @return \Generator
867
     */
868
    public function cursor(Query $query)
869
    {
870
        // 分析查询表达式
871
        $options = $query->getOptions();
872
873
        // 生成查询SQL
874
        $sql = $this->builder->select($query);
875
876
        $bind = $query->getBind();
877
878
        $condition = isset($options['where']['AND']) ? $options['where']['AND'] : null;
879
        $relation  = isset($options['relaltion']) ? $options['relation'] : null;
880
881
        // 执行查询操作
882
        return $this->getCursor($sql, $bind, $options['master'], $query->getModel(), $condition, $relation);
883
    }
884
885
    /**
886
     * 查找记录
887
     * @access public
888
     * @param  Query   $query        查询对象
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 8 found
Loading history...
889
     * @return array|\PDOStatement|string
890
     * @throws DbException
891
     * @throws ModelNotFoundException
892
     * @throws DataNotFoundException
893
     */
894
    public function select(Query $query)
895
    {
896
        // 分析查询表达式
897
        $options = $query->getOptions();
898
899
        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
900
            $resultSet = $this->getCacheData($query, $options['cache'], null, $key);
901
902
            if (false !== $resultSet) {
903
                return $resultSet;
904
            }
905
        }
906
907
        // 生成查询SQL
908
        $sql = $this->builder->select($query);
909
910
        $query->removeOption('limit');
911
912
        $bind = $query->getBind();
913
914
        if (!empty($options['fetch_sql'])) {
915
            // 获取实际执行的SQL语句
916
            return $this->getRealSql($sql, $bind);
917
        }
918
919
        $resultSet = $query->trigger('before_select');
920
921
        if (!$resultSet) {
922
            // 执行查询操作
923
            $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
924
925
            if ($resultSet instanceof \PDOStatement) {
0 ignored issues
show
introduced by
$resultSet is never a sub-type of PDOStatement.
Loading history...
926
                // 返回PDOStatement对象
927
                return $resultSet;
928
            }
929
        }
930
931
        if (!empty($options['cache']) && false !== $resultSet) {
932
            // 缓存数据集
933
            $this->cacheData($key, $resultSet, $options['cache']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
934
        }
935
936
        return $resultSet;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $resultSet also could return the type boolean which is incompatible with the documented return type PDOStatement|array|string.
Loading history...
937
    }
938
939
    /**
940
     * 插入记录
941
     * @access public
942
     * @param  Query   $query        查询对象
943
     * @param  boolean $replace      是否replace
944
     * @param  boolean $getLastInsID 返回自增主键
945
     * @param  string  $sequence     自增序列名
946
     * @return integer|string
947
     */
948
    public function insert(Query $query, $replace = false, $getLastInsID = false, $sequence = null)
949
    {
950
        // 分析查询表达式
951
        $options = $query->getOptions();
952
953
        // 生成SQL语句
954
        $sql = $this->builder->insert($query, $replace);
955
956
        $bind = $query->getBind();
957
958
        if (!empty($options['fetch_sql'])) {
959
            // 获取实际执行的SQL语句
960
            return $this->getRealSql($sql, $bind);
961
        }
962
963
        // 执行操作
964
        $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query);
965
966
        if ($result) {
967
            $sequence  = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
968
            $lastInsId = $this->getLastInsID($sequence);
969
970
            $data = $options['data'];
971
972
            if ($lastInsId) {
973
                $pk = $query->getPk($options);
974
                if (is_string($pk)) {
975
                    $data[$pk] = $lastInsId;
976
                }
977
            }
978
979
            $query->setOption('data', $data);
980
981
            $query->trigger('after_insert');
982
983
            if ($getLastInsID) {
984
                return $lastInsId;
985
            }
986
        }
987
988
        return $result;
989
    }
990
991
    /**
992
     * 批量插入记录
993
     * @access public
994
     * @param  Query     $query      查询对象
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 6 found
Loading history...
995
     * @param  mixed     $dataSet    数据集
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 4 found
Loading history...
996
     * @param  bool      $replace    是否replace
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 4 found
Loading history...
997
     * @param  integer   $limit      每次写入数据限制
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 6 found
Loading history...
998
     * @return integer|string
999
     * @throws \Exception
1000
     * @throws \Throwable
1001
     */
1002
    public function insertAll(Query $query, $dataSet = [], $replace = false, $limit = null)
1003
    {
1004
        if (!is_array(reset($dataSet))) {
1005
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer|string.
Loading history...
1006
        }
1007
1008
        $options = $query->getOptions();
1009
1010
        if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1011
            // 分批写入 自动启动事务支持
1012
            $this->startTrans();
1013
1014
            try {
1015
                $array = array_chunk($dataSet, $limit, true);
1016
                $count = 0;
1017
1018
                foreach ($array as $item) {
1019
                    $sql  = $this->builder->insertAll($query, $item, $replace);
1020
                    $bind = $query->getBind();
1021
1022
                    if (!empty($options['fetch_sql'])) {
1023
                        $fetchSql[] = $this->getRealSql($sql, $bind);
1024
                    } else {
1025
                        $count += $this->execute($sql, $bind, $query);
1026
                    }
1027
                }
1028
1029
                // 提交事务
1030
                $this->commit();
1031
            } catch (\Exception $e) {
1032
                $this->rollback();
1033
                throw $e;
1034
            } catch (\Throwable $e) {
1035
                $this->rollback();
1036
                throw $e;
1037
            }
1038
1039
            return isset($fetchSql) ? implode(';', $fetchSql) : $count;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fetchSql seems to be defined by a foreach iteration on line 1018. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1040
        }
1041
1042
        $sql  = $this->builder->insertAll($query, $dataSet, $replace);
1043
        $bind = $query->getBind();
1044
1045
        if (!empty($options['fetch_sql'])) {
1046
            return $this->getRealSql($sql, $bind);
1047
        }
1048
1049
        return $this->execute($sql, $bind, $query);
1050
    }
1051
1052
    /**
1053
     * 通过Select方式插入记录
1054
     * @access public
1055
     * @param  Query     $query      查询对象
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 6 found
Loading history...
1056
     * @param  string    $fields     要插入的数据表字段名
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 5 found
Loading history...
1057
     * @param  string    $table      要插入的数据表名
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 6 found
Loading history...
1058
     * @return integer|string
1059
     * @throws PDOException
1060
     */
1061
    public function selectInsert(Query $query, $fields, $table)
1062
    {
1063
        // 分析查询表达式
1064
        $options = $query->getOptions();
1065
1066
        $table = $this->parseSqlTable($table);
1067
1068
        $sql = $this->builder->selectInsert($query, $fields, $table);
1069
1070
        $bind = $query->getBind();
1071
1072
        if (!empty($options['fetch_sql'])) {
1073
            return $this->getRealSql($sql, $bind);
1074
        }
1075
1076
        return $this->execute($sql, $bind, $query);
1077
    }
1078
1079
    /**
1080
     * 更新记录
1081
     * @access public
1082
     * @param  Query     $query  查询对象
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 2 found
Loading history...
1083
     * @return integer|string
1084
     * @throws Exception
1085
     * @throws PDOException
1086
     */
1087
    public function update(Query $query)
1088
    {
1089
        $options = $query->getOptions();
1090
1091
        if (isset($options['cache']) && is_string($options['cache']['key'])) {
1092
            $key = $options['cache']['key'];
1093
        }
1094
1095
        $pk   = $query->getPk($options);
1096
        $data = $options['data'];
1097
1098
        if (empty($options['where'])) {
1099
            // 如果存在主键数据 则自动作为更新条件
1100
            if (is_string($pk) && isset($data[$pk])) {
1101
                $where[$pk] = [$pk, '=', $data[$pk]];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$where was never initialized. Although not strictly required by PHP, it is generally a good practice to add $where = array(); before regardless.
Loading history...
1102
                if (!isset($key)) {
1103
                    $key = $this->getCacheKey($query, $data[$pk]);
1104
                }
1105
                unset($data[$pk]);
1106
            } elseif (is_array($pk)) {
1107
                // 增加复合主键支持
1108
                foreach ($pk as $field) {
1109
                    if (isset($data[$field])) {
1110
                        $where[$field] = [$field, '=', $data[$field]];
1111
                    } else {
1112
                        // 如果缺少复合主键数据则不执行
1113
                        throw new Exception('miss complex primary data');
1114
                    }
1115
                    unset($data[$field]);
1116
                }
1117
            }
1118
1119
            if (!isset($where)) {
1120
                // 如果没有任何更新条件则不执行
1121
                throw new Exception('miss update condition');
1122
            } else {
1123
                $options['where']['AND'] = $where;
1124
                $query->setOption('where', ['AND' => $where]);
1125
            }
1126
        } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'])) {
1127
            foreach ($options['where']['AND'] as $val) {
1128
                if (is_array($val) && $val[0] == $pk) {
1129
                    $key = $this->getCacheKey($query, $val);
1130
                }
1131
            }
1132
        }
1133
1134
        // 更新数据
1135
        $query->setOption('data', $data);
1136
1137
        // 生成UPDATE SQL语句
1138
        $sql  = $this->builder->update($query);
1139
        $bind = $query->getBind();
1140
1141
        if (!empty($options['fetch_sql'])) {
1142
            // 获取实际执行的SQL语句
1143
            return $this->getRealSql($sql, $bind);
1144
        }
1145
1146
        // 检测缓存
1147
        $cache = Container::get('cache');
1148
1149
        if (isset($key) && $cache->get($key)) {
1150
            // 删除缓存
1151
            $cache->rm($key);
1152
        } elseif (!empty($options['cache']['tag'])) {
1153
            $cache->clear($options['cache']['tag']);
1154
        }
1155
1156
        // 执行操作
1157
        $result = '' == $sql ? 0 : $this->execute($sql, $bind, $query);
1158
1159
        if ($result) {
1160
            if (is_string($pk) && isset($where[$pk])) {
1161
                $data[$pk] = $where[$pk];
1162
            } elseif (is_string($pk) && isset($key) && strpos($key, '|')) {
1163
                list($a, $val) = explode('|', $key);
1164
                $data[$pk]     = $val;
1165
            }
1166
1167
            $query->setOption('data', $data);
1168
            $query->trigger('after_update');
1169
        }
1170
1171
        return $result;
1172
    }
1173
1174
    /**
1175
     * 删除记录
1176
     * @access public
1177
     * @param  Query $query 查询对象
1178
     * @return int
1179
     * @throws Exception
1180
     * @throws PDOException
1181
     */
1182
    public function delete(Query $query)
1183
    {
1184
        // 分析查询表达式
1185
        $options = $query->getOptions();
1186
        $pk      = $query->getPk($options);
1187
        $data    = $options['data'];
1188
1189
        if (isset($options['cache']) && is_string($options['cache']['key'])) {
1190
            $key = $options['cache']['key'];
1191
        } elseif (!is_null($data) && true !== $data && !is_array($data)) {
1192
            $key = $this->getCacheKey($query, $data);
1193
        } elseif (is_string($pk) && isset($options['where']['AND'])) {
1194
            foreach ($options['where']['AND'] as $val) {
1195
                if (is_array($val) && $val[0] == $pk) {
1196
                    $key = $this->getCacheKey($query, $val);
1197
                }
1198
            }
1199
        }
1200
1201
        if (true !== $data && empty($options['where'])) {
1202
            // 如果条件为空 不进行删除操作 除非设置 1=1
1203
            throw new Exception('delete without condition');
1204
        }
1205
1206
        // 生成删除SQL语句
1207
        $sql = $this->builder->delete($query);
1208
1209
        $bind = $query->getBind();
1210
1211
        if (!empty($options['fetch_sql'])) {
1212
            // 获取实际执行的SQL语句
1213
            return $this->getRealSql($sql, $bind);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getRealSql($sql, $bind) returns the type string which is incompatible with the documented return type integer.
Loading history...
1214
        }
1215
1216
        // 检测缓存
1217
        $cache = Container::get('cache');
1218
1219
        if (isset($key) && $cache->get($key)) {
1220
            // 删除缓存
1221
            $cache->rm($key);
1222
        } elseif (!empty($options['cache']['tag'])) {
1223
            $cache->clear($options['cache']['tag']);
1224
        }
1225
1226
        // 执行操作
1227
        $result = $this->execute($sql, $bind, $query);
1228
1229
        if ($result) {
1230
            if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) {
1231
                list($a, $val) = explode('|', $key);
1232
                $item[$pk]     = $val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$item was never initialized. Although not strictly required by PHP, it is generally a good practice to add $item = array(); before regardless.
Loading history...
1233
                $data          = $item;
1234
            }
1235
1236
            $options['data'] = $data;
1237
1238
            $query->trigger('after_delete');
1239
        }
1240
1241
        return $result;
1242
    }
1243
1244
    /**
1245
     * 得到某个字段的值
1246
     * @access public
1247
     * @param  Query     $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
1248
     * @param  string    $field   字段名
1249
     * @param  bool      $default   默认值
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1250
     * @return mixed
1251
     */
1252
    public function value(Query $query, $field, $default = null)
1253
    {
1254
        $options = $query->getOptions();
1255
1256
        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
1257
            $cache  = $options['cache'];
1258
            $result = $this->getCacheData($query, $cache, null, $key);
1259
1260
            if (false !== $result) {
1261
                return $result;
1262
            }
1263
        }
1264
1265
        if (isset($options['field'])) {
1266
            $query->removeOption('field');
1267
        }
1268
1269
        if (is_string($field)) {
0 ignored issues
show
introduced by
The condition is_string($field) is always true.
Loading history...
1270
            $field = array_map('trim', explode(',', $field));
1271
        }
1272
1273
        $query->setOption('field', $field);
1274
        $query->setOption('limit', 1);
1275
1276
        // 生成查询SQL
1277
        $sql = $this->builder->select($query);
1278
1279
        if (isset($options['field'])) {
1280
            $query->setOption('field', $options['field']);
1281
        } else {
1282
            $query->removeOption('field');
1283
        }
1284
1285
        $query->removeOption('limit');
1286
1287
        $bind = $query->getBind();
1288
1289
        if (!empty($options['fetch_sql'])) {
1290
            // 获取实际执行的SQL语句
1291
            return $this->getRealSql($sql, $bind);
1292
        }
1293
1294
        // 执行查询操作
1295
        $pdo = $this->query($sql, $bind, $options['master'], true);
1296
1297
        $result = $pdo->fetchColumn();
1298
1299
        if (isset($cache) && false !== $result) {
1300
            // 缓存数据
1301
            $this->cacheData($key, $result, $cache);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
1302
        }
1303
1304
        return false !== $result ? $result : $default;
1305
    }
1306
1307
    /**
1308
     * 得到某个字段的值
1309
     * @access public
1310
     * @param  Query     $query     查询对象
1311
     * @param  string    $aggregate 聚合方法
1312
     * @param  mixed     $field     字段名
1313
     * @return mixed
1314
     */
1315
    public function aggregate(Query $query, $aggregate, $field)
1316
    {
1317
        if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
1318
            list($distinct, $field) = explode(' ', $field);
1319
        }
1320
1321
        $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
1322
1323
        return $this->value($query, $field, 0);
1324
    }
1325
1326
    /**
1327
     * 得到某个列的数组
1328
     * @access public
1329
     * @param  Query     $query 查询对象
1330
     * @param  string    $field 字段名 多个字段用逗号分隔
1331
     * @param  string    $key   索引
1332
     * @return array
1333
     */
1334
    public function column(Query $query, $field, $key = '')
1335
    {
1336
        $options = $query->getOptions();
1337
1338
        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
1339
            // 判断查询缓存
1340
            $cache  = $options['cache'];
1341
            $result = $this->getCacheData($query, $cache, null, $guid);
1342
1343
            if (false !== $result) {
1344
                return $result;
1345
            }
1346
        }
1347
1348
        if (isset($options['field'])) {
1349
            $query->removeOption('field');
1350
        }
1351
1352
        if (is_null($field)) {
0 ignored issues
show
introduced by
The condition is_null($field) is always false.
Loading history...
1353
            $field = ['*'];
1354
        } elseif (is_string($field)) {
0 ignored issues
show
introduced by
The condition is_string($field) is always true.
Loading history...
1355
            $field = array_map('trim', explode(',', $field));
1356
        }
1357
1358
        if ($key && ['*'] != $field) {
1359
            array_unshift($field, $key);
1360
            $field = array_unique($field);
1361
        }
1362
1363
        $query->setOption('field', $field);
1364
1365
        // 生成查询SQL
1366
        $sql = $this->builder->select($query);
1367
1368
        // 还原field参数
1369
        if (isset($options['field'])) {
1370
            $query->setOption('field', $options['field']);
1371
        } else {
1372
            $query->removeOption('field');
1373
        }
1374
1375
        $bind = $query->getBind();
1376
1377
        if (!empty($options['fetch_sql'])) {
1378
            // 获取实际执行的SQL语句
1379
            return $this->getRealSql($sql, $bind);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getRealSql($sql, $bind) returns the type string which is incompatible with the documented return type array.
Loading history...
1380
        }
1381
1382
        // 执行查询操作
1383
        $pdo = $this->query($sql, $bind, $options['master'], true);
1384
1385
        if (1 == $pdo->columnCount()) {
1386
            $result = $pdo->fetchAll(PDO::FETCH_COLUMN);
1387
        } else {
1388
            $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
1389
1390
            if (['*'] == $field && $key) {
1391
                $result = array_column($resultSet, null, $key);
1392
            } elseif ($resultSet) {
1393
                $fields = array_keys($resultSet[0]);
1394
                $count  = count($fields);
1395
                $key1   = array_shift($fields);
1396
                $key2   = $fields ? array_shift($fields) : '';
1397
                $key    = $key ?: $key1;
1398
1399
                if (strpos($key, '.')) {
1400
                    list($alias, $key) = explode('.', $key);
1401
                }
1402
1403
                if (2 == $count) {
1404
                    $column = $key2;
1405
                } elseif (1 == $count) {
1406
                    $column = $key1;
1407
                } else {
1408
                    $column = null;
1409
                }
1410
1411
                $result = array_column($resultSet, $column, $key);
1412
            } else {
1413
                $result = [];
1414
            }
1415
        }
1416
1417
        if (isset($cache) && isset($guid)) {
1418
            // 缓存数据
1419
            $this->cacheData($guid, $result, $cache);
1420
        }
1421
1422
        return $result;
1423
    }
1424
1425
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $query should have a doc-comment as per coding-style.
Loading history...
1426
     * 执行查询但只返回PDOStatement对象
1427
     * @access public
1428
     * @return \PDOStatement|string
1429
     */
1430
    public function pdo(Query $query)
1431
    {
1432
        // 分析查询表达式
1433
        $options = $query->getOptions();
1434
1435
        // 生成查询SQL
1436
        $sql = $this->builder->select($query);
1437
1438
        $bind = $query->getBind();
1439
1440
        if (!empty($options['fetch_sql'])) {
1441
            // 获取实际执行的SQL语句
1442
            return $this->getRealSql($sql, $bind);
1443
        }
1444
1445
        // 执行查询操作
1446
        return $this->query($sql, $bind, $options['master'], true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->query($sql...ptions['master'], true) returns the type array which is incompatible with the documented return type PDOStatement|string.
Loading history...
1447
    }
1448
1449
    /**
1450
     * 根据参数绑定组装最终的SQL语句 便于调试
1451
     * @access public
1452
     * @param  string    $sql 带参数绑定的sql语句
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1453
     * @param  array     $bind 参数绑定列表
1454
     * @return string
1455
     */
1456
    public function getRealSql($sql, array $bind = [])
1457
    {
1458
        if (is_array($sql)) {
0 ignored issues
show
introduced by
The condition is_array($sql) is always false.
Loading history...
1459
            $sql = implode(';', $sql);
1460
        }
1461
1462
        foreach ($bind as $key => $val) {
1463
            $value = is_array($val) ? $val[0] : $val;
1464
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
1465
1466
            if (self::PARAM_FLOAT == $type) {
1467
                $value = (float) $value;
1468
            } elseif (PDO::PARAM_STR == $type) {
1469
                $value = '\'' . addslashes($value) . '\'';
1470
            } elseif (PDO::PARAM_INT == $type && '' === $value) {
1471
                $value = 0;
1472
            }
1473
1474
            // 判断占位符
1475
            $sql = is_numeric($key) ?
1476
            substr_replace($sql, $value, strpos($sql, '?'), 1) :
1477
            substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
1478
        }
1479
1480
        return rtrim($sql);
1481
    }
1482
1483
    /**
1484
     * 参数绑定
1485
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
1486
     * 或者 ['value',123] 对应问号占位符
1487
     * @access public
1488
     * @param  array $bind 要绑定的参数列表
1489
     * @return void
1490
     * @throws BindParamException
1491
     */
1492
    protected function bindValue(array $bind = [])
1493
    {
1494
        foreach ($bind as $key => $val) {
1495
            // 占位符
1496
            $param = is_int($key) ? $key + 1 : ':' . $key;
1497
1498
            if (is_array($val)) {
1499
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
1500
                    $val[0] = 0;
1501
                } elseif (self::PARAM_FLOAT == $val[1]) {
1502
                    $val[0] = (float) $val[0];
1503
                    $val[1] = PDO::PARAM_STR;
1504
                }
1505
1506
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
1507
            } else {
1508
                $result = $this->PDOStatement->bindValue($param, $val);
1509
            }
1510
1511
            if (!$result) {
1512
                throw new BindParamException(
1513
                    "Error occurred  when binding parameters '{$param}'",
1514
                    $this->config,
1515
                    $this->getLastsql(),
1516
                    $bind
1517
                );
1518
            }
1519
        }
1520
    }
1521
1522
    /**
1523
     * 存储过程的输入输出参数绑定
1524
     * @access public
1525
     * @param  array $bind 要绑定的参数列表
1526
     * @return void
1527
     * @throws BindParamException
1528
     */
1529
    protected function bindParam($bind)
1530
    {
1531
        foreach ($bind as $key => $val) {
1532
            $param = is_int($key) ? $key + 1 : ':' . $key;
1533
1534
            if (is_array($val)) {
1535
                array_unshift($val, $param);
1536
                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
1537
            } else {
1538
                $result = $this->PDOStatement->bindValue($param, $val);
1539
            }
1540
1541
            if (!$result) {
1542
                $param = array_shift($val);
1543
1544
                throw new BindParamException(
1545
                    "Error occurred  when binding parameters '{$param}'",
1546
                    $this->config,
1547
                    $this->getLastsql(),
1548
                    $bind
1549
                );
1550
            }
1551
        }
1552
    }
1553
1554
    /**
1555
     * 获得数据集数组
1556
     * @access protected
1557
     * @param  bool   $pdo 是否返回PDOStatement
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
1558
     * @param  bool   $procedure 是否存储过程
1559
     * @return array
1560
     */
1561
    protected function getResult($pdo = false, $procedure = false)
1562
    {
1563
        if ($pdo) {
1564
            // 返回PDOStatement对象处理
1565
            return $this->PDOStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->PDOStatement returns the type PDOStatement which is incompatible with the documented return type array.
Loading history...
1566
        }
1567
1568
        if ($procedure) {
1569
            // 存储过程返回结果
1570
            return $this->procedure();
1571
        }
1572
1573
        $result = $this->PDOStatement->fetchAll($this->fetchType);
1574
1575
        $this->numRows = count($result);
1576
1577
        return $result;
1578
    }
1579
1580
    /**
1581
     * 获得存储过程数据集
1582
     * @access protected
1583
     * @return array
1584
     */
1585
    protected function procedure()
1586
    {
1587
        $item = [];
1588
1589
        do {
1590
            $result = $this->getResult();
1591
            if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1592
                $item[] = $result;
1593
            }
1594
        } while ($this->PDOStatement->nextRowset());
1595
1596
        $this->numRows = count($item);
1597
1598
        return $item;
1599
    }
1600
1601
    /**
1602
     * 执行数据库事务
1603
     * @access public
1604
     * @param  callable $callback 数据操作方法回调
1605
     * @return mixed
1606
     * @throws PDOException
1607
     * @throws \Exception
1608
     * @throws \Throwable
1609
     */
1610
    public function transaction($callback)
1611
    {
1612
        $this->startTrans();
1613
1614
        try {
1615
            $result = null;
1616
            if (is_callable($callback)) {
1617
                $result = call_user_func_array($callback, [$this]);
1618
            }
1619
1620
            $this->commit();
1621
            return $result;
1622
        } catch (\Exception $e) {
1623
            $this->rollback();
1624
            throw $e;
1625
        } catch (\Throwable $e) {
1626
            $this->rollback();
1627
            throw $e;
1628
        }
1629
    }
1630
1631
    /**
1632
     * 启动XA事务
1633
     * @access public
1634
     * @param  string $xid XA事务id
1635
     * @return void
1636
     */
1637
    public function startTransXa($xid)
0 ignored issues
show
Unused Code introduced by
The parameter $xid is not used and could be removed. ( Ignorable by Annotation )

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

1637
    public function startTransXa(/** @scrutinizer ignore-unused */ $xid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1638
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
1639
1640
    /**
1641
     * 预编译XA事务
1642
     * @access public
1643
     * @param  string $xid XA事务id
1644
     * @return void
1645
     */
1646
    public function prepareXa($xid)
0 ignored issues
show
Unused Code introduced by
The parameter $xid is not used and could be removed. ( Ignorable by Annotation )

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

1646
    public function prepareXa(/** @scrutinizer ignore-unused */ $xid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1647
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
1648
1649
    /**
1650
     * 提交XA事务
1651
     * @access public
1652
     * @param  string $xid XA事务id
1653
     * @return void
1654
     */
1655
    public function commitXa($xid)
0 ignored issues
show
Unused Code introduced by
The parameter $xid is not used and could be removed. ( Ignorable by Annotation )

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

1655
    public function commitXa(/** @scrutinizer ignore-unused */ $xid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1656
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
1657
1658
    /**
1659
     * 回滚XA事务
1660
     * @access public
1661
     * @param  string $xid XA事务id
1662
     * @return void
1663
     */
1664
    public function rollbackXa($xid)
0 ignored issues
show
Unused Code introduced by
The parameter $xid is not used and could be removed. ( Ignorable by Annotation )

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

1664
    public function rollbackXa(/** @scrutinizer ignore-unused */ $xid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1665
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
1666
1667
    /**
1668
     * 启动事务
1669
     * @access public
1670
     * @return void
1671
     * @throws \PDOException
1672
     * @throws \Exception
1673
     */
1674
    public function startTrans()
1675
    {
1676
        $this->initConnect(true);
1677
        if (!$this->linkID) {
1678
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type void.
Loading history...
1679
        }
1680
1681
        ++$this->transTimes;
1682
1683
        try {
1684
            if (1 == $this->transTimes) {
1685
                $this->linkID->beginTransaction();
1686
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1687
                $this->linkID->exec(
1688
                    $this->parseSavepoint('trans' . $this->transTimes)
1689
                );
1690
            }
1691
        } catch (\Exception $e) {
1692
            if ($this->isBreak($e)) {
1693
                --$this->transTimes;
1694
                return $this->close()->startTrans();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->close()->startTrans() targeting think\db\Connection::startTrans() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1695
            }
1696
            throw $e;
1697
        }
1698
    }
1699
1700
    /**
1701
     * 用于非自动提交状态下面的查询提交
1702
     * @access public
1703
     * @return void
1704
     * @throws PDOException
1705
     */
1706
    public function commit()
1707
    {
1708
        $this->initConnect(true);
1709
1710
        if (1 == $this->transTimes) {
1711
            $this->linkID->commit();
1712
        }
1713
1714
        --$this->transTimes;
1715
    }
1716
1717
    /**
1718
     * 事务回滚
1719
     * @access public
1720
     * @return void
1721
     * @throws PDOException
1722
     */
1723
    public function rollback()
1724
    {
1725
        $this->initConnect(true);
1726
1727
        if (1 == $this->transTimes) {
1728
            $this->linkID->rollBack();
1729
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1730
            $this->linkID->exec(
1731
                $this->parseSavepointRollBack('trans' . $this->transTimes)
1732
            );
1733
        }
1734
1735
        $this->transTimes = max(0, $this->transTimes - 1);
1736
    }
1737
1738
    /**
1739
     * 是否支持事务嵌套
1740
     * @return bool
1741
     */
1742
    protected function supportSavepoint()
1743
    {
1744
        return false;
1745
    }
1746
1747
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $name should have a doc-comment as per coding-style.
Loading history...
1748
     * 生成定义保存点的SQL
1749
     * @access protected
1750
     * @param  $name
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
1751
     * @return string
1752
     */
1753
    protected function parseSavepoint($name)
1754
    {
1755
        return 'SAVEPOINT ' . $name;
1756
    }
1757
1758
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $name should have a doc-comment as per coding-style.
Loading history...
1759
     * 生成回滚到保存点的SQL
1760
     * @access protected
1761
     * @param  $name
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
1762
     * @return string
1763
     */
1764
    protected function parseSavepointRollBack($name)
1765
    {
1766
        return 'ROLLBACK TO SAVEPOINT ' . $name;
1767
    }
1768
1769
    /**
1770
     * 批处理执行SQL语句
1771
     * 批处理的指令都认为是execute操作
1772
     * @access public
1773
     * @param  array $sqlArray   SQL批处理指令
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1774
     * @param  array $bind       参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 7 found
Loading history...
1775
     * @return boolean
1776
     */
1777
    public function batchQuery($sqlArray = [], $bind = [])
1778
    {
1779
        if (!is_array($sqlArray)) {
0 ignored issues
show
introduced by
The condition is_array($sqlArray) is always true.
Loading history...
1780
            return false;
1781
        }
1782
1783
        // 自动启动事务支持
1784
        $this->startTrans();
1785
1786
        try {
1787
            foreach ($sqlArray as $sql) {
1788
                $this->execute($sql, $bind);
1789
            }
1790
            // 提交事务
1791
            $this->commit();
1792
        } catch (\Exception $e) {
1793
            $this->rollback();
1794
            throw $e;
1795
        }
1796
1797
        return true;
1798
    }
1799
1800
    /**
1801
     * 获得查询次数
1802
     * @access public
1803
     * @param  boolean $execute 是否包含所有查询
1804
     * @return integer
1805
     */
1806
    public function getQueryTimes($execute = false)
1807
    {
1808
        return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes;
1809
    }
1810
1811
    /**
1812
     * 获得执行次数
1813
     * @access public
1814
     * @return integer
1815
     */
1816
    public function getExecuteTimes()
1817
    {
1818
        return Db::$executeTimes;
1819
    }
1820
1821
    /**
1822
     * 关闭数据库(或者重新连接)
1823
     * @access public
1824
     * @return $this
1825
     */
1826
    public function close()
1827
    {
1828
        $this->linkID    = null;
1829
        $this->linkWrite = null;
1830
        $this->linkRead  = null;
1831
        $this->links     = [];
1832
1833
        // 释放查询
1834
        $this->free();
1835
1836
        return $this;
1837
    }
1838
1839
    /**
1840
     * 是否断线
1841
     * @access protected
1842
     * @param  \PDOException|\Exception  $e 异常对象
1843
     * @return bool
1844
     */
1845
    protected function isBreak($e)
1846
    {
1847
        if (!$this->config['break_reconnect']) {
1848
            return false;
1849
        }
1850
1851
        $error = $e->getMessage();
1852
1853
        foreach ($this->breakMatchStr as $msg) {
1854
            if (false !== stripos($error, $msg)) {
1855
                return true;
1856
            }
1857
        }
1858
        return false;
1859
    }
1860
1861
    /**
1862
     * 获取最近一次查询的sql语句
1863
     * @access public
1864
     * @return string
1865
     */
1866
    public function getLastSql()
1867
    {
1868
        return $this->getRealSql($this->queryStr, $this->bind);
1869
    }
1870
1871
    /**
1872
     * 获取最近插入的ID
1873
     * @access public
1874
     * @param  string  $sequence     自增序列名
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 5 found
Loading history...
1875
     * @return string
1876
     */
1877
    public function getLastInsID($sequence = null)
1878
    {
1879
        return $this->linkID->lastInsertId($sequence);
1880
    }
1881
1882
    /**
1883
     * 获取返回或者影响的记录数
1884
     * @access public
1885
     * @return integer
1886
     */
1887
    public function getNumRows()
1888
    {
1889
        return $this->numRows;
1890
    }
1891
1892
    /**
1893
     * 获取最近的错误信息
1894
     * @access public
1895
     * @return string
1896
     */
1897
    public function getError()
1898
    {
1899
        if ($this->PDOStatement) {
1900
            $error = $this->PDOStatement->errorInfo();
1901
            $error = $error[1] . ':' . $error[2];
1902
        } else {
1903
            $error = '';
1904
        }
1905
1906
        if ('' != $this->queryStr) {
1907
            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
1908
        }
1909
1910
        return $error;
1911
    }
1912
1913
    /**
1914
     * 数据库调试 记录当前SQL及分析性能
1915
     * @access protected
1916
     * @param  boolean $start 调试开始标记 true 开始 false 结束
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1917
     * @param  string  $sql 执行的SQL语句 留空自动获取
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
1918
     * @param  bool    $master 主从标记
1919
     * @return void
1920
     */
1921
    protected function debug($start, $sql = '', $master = false)
1922
    {
1923
        if (!empty($this->config['debug'])) {
1924
            // 开启数据库调试模式
1925
            $debug = Container::get('debug');
1926
1927
            if ($start) {
1928
                $debug->remark('queryStartTime', 'time');
1929
            } else {
1930
                // 记录操作结束时间
1931
                $debug->remark('queryEndTime', 'time');
1932
                $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime');
1933
                $sql     = $sql ?: $this->getLastsql();
1934
                $result  = [];
1935
1936
                // SQL性能分析
1937
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
1938
                    $result = $this->getExplain($sql);
1939
                }
1940
1941
                // SQL监听
1942
                $this->triggerSql($sql, $runtime, $result, $master);
1943
            }
1944
        }
1945
    }
1946
1947
    /**
1948
     * 监听SQL执行
1949
     * @access public
1950
     * @param  callable $callback 回调方法
1951
     * @return void
1952
     */
1953
    public function listen($callback)
1954
    {
1955
        self::$event[] = $callback;
1956
    }
1957
1958
    /**
1959
     * 触发SQL事件
1960
     * @access protected
1961
     * @param  string    $sql SQL语句
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
1962
     * @param  float     $runtime SQL运行时间
1963
     * @param  mixed     $explain SQL分析
1964
     * @param  bool      $master 主从标记
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1965
     * @return void
1966
     */
1967
    protected function triggerSql($sql, $runtime, $explain = [], $master = false)
1968
    {
1969
        if (!empty(self::$event)) {
1970
            foreach (self::$event as $callback) {
1971
                if (is_callable($callback)) {
1972
                    call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
1973
                }
1974
            }
1975
        } else {
1976
            if ($this->config['deploy']) {
1977
                // 分布式记录当前操作的主从
1978
                $master = $master ? 'master|' : 'slave|';
1979
            } else {
1980
                $master = '';
1981
            }
1982
1983
            // 未注册监听则记录到日志中
1984
            $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
1985
1986
            if (!empty($explain)) {
1987
                $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
1988
            }
1989
        }
1990
    }
1991
1992
    public function log($log, $type = 'sql')
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
1993
    {
1994
        $this->config['debug'] && Container::get('log')->record($log, $type);
1995
    }
1996
1997
    /**
1998
     * 初始化数据库连接
1999
     * @access protected
2000
     * @param  boolean $master 是否主服务器
2001
     * @return void
2002
     */
2003
    protected function initConnect($master = true)
2004
    {
2005
        if (!empty($this->config['deploy'])) {
2006
            // 采用分布式数据库
2007
            if ($master || $this->transTimes) {
2008
                if (!$this->linkWrite) {
2009
                    $this->linkWrite = $this->multiConnect(true);
2010
                }
2011
2012
                $this->linkID = $this->linkWrite;
2013
            } else {
2014
                if (!$this->linkRead) {
2015
                    $this->linkRead = $this->multiConnect(false);
2016
                }
2017
2018
                $this->linkID = $this->linkRead;
2019
            }
2020
        } elseif (!$this->linkID) {
2021
            // 默认单数据库
2022
            $this->linkID = $this->connect();
2023
        }
2024
    }
2025
2026
    /**
2027
     * 连接分布式服务器
2028
     * @access protected
2029
     * @param  boolean $master 主服务器
2030
     * @return PDO
2031
     */
2032
    protected function multiConnect($master = false)
2033
    {
2034
        $_config = [];
2035
2036
        // 分布式数据库配置解析
2037
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
2038
            $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
2039
        }
2040
2041
        // 主服务器序号
2042
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
2043
2044
        if ($this->config['rw_separate']) {
2045
            // 主从式采用读写分离
2046
            if ($master) // 主服务器写入
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...) // 主服务器写入\n {\n"
Loading history...
2047
            {
2048
                $r = $m;
2049
            } elseif (is_numeric($this->config['slave_no'])) {
2050
                // 指定服务器读
2051
                $r = $this->config['slave_no'];
2052
            } else {
2053
                // 读操作连接从服务器 每次随机连接的数据库
2054
                $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
2055
            }
2056
        } else {
2057
            // 读写操作不区分服务器 每次随机连接的数据库
2058
            $r = floor(mt_rand(0, count($_config['hostname']) - 1));
2059
        }
2060
        $dbMaster = false;
2061
2062
        if ($m != $r) {
2063
            $dbMaster = [];
2064
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
2065
                $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0];
2066
            }
2067
        }
2068
2069
        $dbConfig = [];
2070
2071
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
2072
            $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0];
2073
        }
2074
2075
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
2076
    }
2077
2078
    /**
2079
     * 析构方法
2080
     * @access public
2081
     */
2082
    public function __destruct()
2083
    {
2084
        // 关闭连接
2085
        $this->close();
2086
    }
2087
2088
    /**
2089
     * 缓存数据
2090
     * @access protected
2091
     * @param  string    $key    缓存标识
2092
     * @param  mixed     $data   缓存数据
2093
     * @param  array     $config 缓存参数
2094
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
2095
    protected function cacheData($key, $data, $config = [])
2096
    {
2097
        $cache = Container::get('cache');
2098
2099
        if (isset($config['tag'])) {
2100
            $cache->tag($config['tag'])->set($key, $data, $config['expire']);
2101
        } else {
2102
            $cache->set($key, $data, $config['expire']);
2103
        }
2104
    }
2105
2106
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $data should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $key should have a doc-comment as per coding-style.
Loading history...
2107
     * 获取缓存数据
2108
     * @access protected
2109
     * @param  Query     $query   查询对象
2110
     * @param  mixed     $cache   缓存设置
2111
     * @param  array     $options 缓存
0 ignored issues
show
Coding Style introduced by
Doc comment for parameter $options does not match actual variable name $data
Loading history...
2112
     * @return mixed
2113
     */
2114
    protected function getCacheData(Query $query, $cache, $data, &$key = null)
2115
    {
2116
        // 判断查询缓存
2117
        $key = is_string($cache['key']) ? $cache['key'] : $this->getCacheKey($query, $data);
2118
2119
        return Container::get('cache')->get($key);
2120
    }
2121
2122
    /**
2123
     * 生成缓存标识
2124
     * @access protected
2125
     * @param  Query     $query   查询对象
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
2126
     * @param  mixed     $value   缓存数据
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
2127
     * @return string
2128
     */
2129
    protected function getCacheKey(Query $query, $value)
2130
    {
2131
        if (is_scalar($value)) {
2132
            $data = $value;
2133
        } elseif (is_array($value) && isset($value[1], $value[2]) && in_array($value[1], ['=', 'eq'], true) && is_scalar($value[2])) {
2134
            $data = $value[2];
2135
        }
2136
2137
        $prefix = 'think:' . $this->getConfig('database') . '.';
0 ignored issues
show
Bug introduced by
Are you sure $this->getConfig('database') of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

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

2137
        $prefix = 'think:' . /** @scrutinizer ignore-type */ $this->getConfig('database') . '.';
Loading history...
2138
2139
        if (isset($data)) {
2140
            return $prefix . $query->getTable() . '|' . $data;
2141
        }
2142
2143
        try {
2144
            return md5($prefix . serialize($query->getOptions()) . serialize($query->getBind(false)));
2145
        } catch (\Exception $e) {
2146
            throw new Exception('closure not support cache(true)');
2147
        }
2148
    }
2149
2150
}
2151