Passed
Push — 5.2 ( 413287...90fdf8 )
by liu
02:33
created

Connection::getQueryTimes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
// +----------------------------------------------------------------------
1 ignored issue
show
Coding Style introduced by
You must use "/**" style comments for a file comment
Loading history...
3
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
4
// +----------------------------------------------------------------------
5
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
6
// +----------------------------------------------------------------------
7
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
8
// +----------------------------------------------------------------------
9
// | Author: liu21st <[email protected]>
10
// +----------------------------------------------------------------------
11
declare (strict_types = 1);
12
13
namespace think\db;
14
15
use InvalidArgumentException;
16
use PDO;
17
use PDOStatement;
18
use think\App;
19
use think\Cache;
20
use think\cache\CacheItem;
21
use think\Container;
22
use think\Db;
23
use think\db\exception\BindParamException;
24
use think\Debug;
25
use think\Exception;
26
use think\exception\PDOException;
27
28
abstract class Connection
1 ignored issue
show
Coding Style introduced by
Missing class doc comment
Loading history...
29
{
30
    const PARAM_FLOAT = 21;
31
32
    /**
33
     * 数据库连接实例
34
     * @var array
35
     */
36
    protected static $instance = [];
37
38
    /**
39
     * PDO操作实例
40
     * @var PDOStatement
41
     */
42
    protected $PDOStatement;
43
44
    /**
45
     * 当前SQL指令
46
     * @var string
47
     */
48
    protected $queryStr = '';
49
50
    /**
51
     * 返回或者影响记录数
52
     * @var int
53
     */
54
    protected $numRows = 0;
55
56
    /**
57
     * 事务指令数
58
     * @var int
59
     */
60
    protected $transTimes = 0;
61
62
    /**
63
     * 错误信息
64
     * @var string
65
     */
66
    protected $error = '';
67
68
    /**
69
     * 数据库连接ID 支持多个连接
70
     * @var PDO[]
71
     */
72
    protected $links = [];
73
74
    /**
75
     * 当前连接ID
76
     * @var PDO
77
     */
78
    protected $linkID;
79
80
    /**
81
     * 当前读连接ID
82
     * @var PDO
83
     */
84
    protected $linkRead;
85
86
    /**
87
     * 当前写连接ID
88
     * @var PDO
89
     */
90
    protected $linkWrite;
91
92
    /**
93
     * 查询结果类型
94
     * @var int
95
     */
96
    protected $fetchType = PDO::FETCH_ASSOC;
97
98
    /**
99
     * 字段属性大小写
100
     * @var int
101
     */
102
    protected $attrCase = PDO::CASE_LOWER;
103
104
    /**
105
     * 监听回调
106
     * @var array
107
     */
108
    protected static $event = [];
109
110
    /**
111
     * 数据表信息
112
     * @var array
113
     */
114
    protected static $info = [];
115
116
    /**
117
     * Builder类名
118
     * @var string
119
     */
120
    protected $builderClassName;
121
122
    /**
123
     * Builder对象
124
     * @var Builder
125
     */
126
    protected $builder;
127
128
    /**
129
     * 数据库连接参数配置
130
     * @var array
131
     */
132
    protected $config = [
133
        // 数据库类型
134
        'type'            => '',
135
        // 服务器地址
136
        'hostname'        => '',
137
        // 数据库名
138
        'database'        => '',
139
        // 用户名
140
        'username'        => '',
141
        // 密码
142
        'password'        => '',
143
        // 端口
144
        'hostport'        => '',
145
        // 连接dsn
146
        'dsn'             => '',
147
        // 数据库连接参数
148
        'params'          => [],
149
        // 数据库编码默认采用utf8
150
        'charset'         => 'utf8',
151
        // 数据库表前缀
152
        'prefix'          => '',
153
        // 数据库调试模式
154
        'debug'           => false,
155
        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
156
        'deploy'          => 0,
157
        // 数据库读写是否分离 主从式有效
158
        'rw_separate'     => false,
159
        // 读写分离后 主服务器数量
160
        'master_num'      => 1,
161
        // 指定从服务器序号
162
        'slave_no'        => '',
163
        // 模型写入后自动读取主服务器
164
        'read_master'     => false,
165
        // 是否严格检查字段是否存在
166
        'fields_strict'   => true,
167
        // 自动写入时间戳字段
168
        'auto_timestamp'  => false,
169
        // 时间字段取出后的默认时间格式
170
        'datetime_format' => 'Y-m-d H:i:s',
171
        // 是否需要进行SQL性能分析
172
        'sql_explain'     => false,
173
        // Builder类
174
        'builder'         => '',
175
        // Query类
176
        'query'           => '\\think\\db\\Query',
177
        // 是否需要断线重连
178
        'break_reconnect' => false,
179
        // 断线标识字符串
180
        'break_match_str' => [],
181
    ];
182
183
    /**
184
     * PDO连接参数
185
     * @var array
186
     */
187
    protected $params = [
188
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
189
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
190
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
191
        PDO::ATTR_STRINGIFY_FETCHES => false,
192
        PDO::ATTR_EMULATE_PREPARES  => false,
193
    ];
194
195
    /**
196
     * 参数绑定类型映射
197
     * @var array
198
     */
199
    protected $bindType = [
200
        'string'  => PDO::PARAM_STR,
201
        'str'     => PDO::PARAM_STR,
202
        'integer' => PDO::PARAM_INT,
203
        'int'     => PDO::PARAM_INT,
204
        'boolean' => PDO::PARAM_BOOL,
205
        'bool'    => PDO::PARAM_BOOL,
206
        'float'   => self::PARAM_FLOAT,
207
    ];
208
209
    /**
210
     * 服务器断线标识字符
211
     * @var array
212
     */
213
    protected $breakMatchStr = [
214
        'server has gone away',
215
        'no connection to the server',
216
        'Lost connection',
217
        'is dead or not enabled',
218
        'Error while sending',
219
        'decryption failed or bad record mac',
220
        'server closed the connection unexpectedly',
221
        'SSL connection has been closed unexpectedly',
222
        'Error writing data to the connection',
223
        'Resource deadlock avoided',
224
        'failed with errno',
225
    ];
226
227
    /**
228
     * 绑定参数
229
     * @var array
230
     */
231
    protected $bind = [];
232
233
    /**
234
     * 缓存对象
235
     * @var Cache
236
     */
237
    protected $cache;
238
239
    /**
240
     * 架构函数 读取数据库配置信息
241
     * @access public
242
     * @param  array $config 数据库配置数组
243
     */
244
    public function __construct(array $config = [])
245
    {
246
        if (!empty($config)) {
247
            $this->config = array_merge($this->config, $config);
248
        }
249
250
        // 创建Builder对象
251
        $class = $this->getBuilderClass();
252
253
        $this->builder = new $class($this);
254
        $this->cache   = Container::pull('cache');
255
256
        // 执行初始化操作
257
        $this->initialize();
258
    }
259
260
    /**
261
     * 初始化
262
     * @access protected
263
     * @return void
264
     */
265
    protected function initialize(): void
266
    {}
0 ignored issues
show
Coding Style introduced by
Closing brace must be on a line by itself
Loading history...
267
268
    /**
269
     * 取得数据库连接类实例
270
     * @access public
271
     * @param  array       $config 连接配置
272
     * @param  bool|string $name 连接标识 true 强制重新连接
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
273
     * @return Connection
274
     * @throws Exception
275
     */
276
    public static function instance(array $config = [], $name = false)
277
    {
278
        if (false === $name) {
279
            $name = md5(serialize($config));
280
        }
281
282
        if (true === $name || !isset(self::$instance[$name])) {
283
284
            if (empty($config['type'])) {
285
                throw new InvalidArgumentException('Undefined db type');
286
            }
287
288
            // 记录初始化信息
289
            Container::pull('log')->record('[ DB ] INIT ' . $config['type']);
290
291
            if (true === $name) {
292
                $name = md5(serialize($config));
293
            }
294
295
            self::$instance[$name] = App::factory($config['type'], '\\think\\db\\connector\\', $config);
296
        }
297
298
        return self::$instance[$name];
299
    }
300
301
    /**
302
     * 获取当前连接器类对应的Builder类
303
     * @access public
304
     * @return string
305
     */
306
    public function getBuilderClass(): string
307
    {
308
        if (!empty($this->builderClassName)) {
309
            return $this->builderClassName;
310
        }
311
312
        return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
313
    }
314
315
    /**
316
     * 设置当前的数据库Builder对象
317
     * @access protected
318
     * @param  Builder $builder
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
319
     * @return $this
320
     */
321
    protected function setBuilder(Builder $builder)
322
    {
323
        $this->builder = $builder;
324
325
        return $this;
326
    }
327
328
    /**
329
     * 获取当前的builder实例对象
330
     * @access public
331
     * @return Builder
332
     */
333
    public function getBuilder(): Builder
334
    {
335
        return $this->builder;
336
    }
337
338
    /**
339
     * 解析pdo连接的dsn信息
340
     * @access protected
341
     * @param  array $config 连接信息
342
     * @return string
343
     */
344
    abstract protected function parseDsn(array $config);
345
346
    /**
347
     * 取得数据表的字段信息
348
     * @access public
349
     * @param  string $tableName 数据表名称
350
     * @return array
351
     */
352
    abstract public function getFields(string $tableName);
353
354
    /**
355
     * 取得数据库的表信息
356
     * @access public
357
     * @param string $dbName 数据库名称
1 ignored issue
show
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 1
Loading history...
358
     * @return array
359
     */
360
    abstract public function getTables(string $dbName);
361
362
    /**
363
     * SQL性能分析
364
     * @access protected
365
     * @param  string $sql SQL语句
366
     * @return array
367
     */
368
    abstract protected function getExplain(string $sql);
369
370
    /**
371
     * 对返数据表字段信息进行大小写转换出来
372
     * @access public
373
     * @param  array $info 字段信息
374
     * @return array
375
     */
376
    public function fieldCase(array $info): array
377
    {
378
        // 字段大小写转换
379
        switch ($this->attrCase) {
380
            case PDO::CASE_LOWER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
381
                $info = array_change_key_case($info);
382
                break;
383
            case PDO::CASE_UPPER:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
384
                $info = array_change_key_case($info, CASE_UPPER);
385
                break;
386
            case PDO::CASE_NATURAL:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
387
            default:
1 ignored issue
show
Coding Style introduced by
Line indented incorrectly; expected 8 spaces, found 12
Loading history...
388
                // 不做转换
389
        }
390
391
        return $info;
392
    }
393
394
    /**
395
     * 获取字段绑定类型
396
     * @access public
397
     * @param  string $type 字段类型
398
     * @return integer
399
     */
400
    public function getFieldBindType(string $type): int
401
    {
402
        if (in_array($type, ['integer', 'string', 'float', 'boolean', 'bool', 'int', 'str'])) {
403
            $bind = $this->bindType[$type];
404
        } elseif (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
405
            $bind = PDO::PARAM_STR;
406
        } elseif (preg_match('/(double|float|decimal|real|numeric)/is', $type)) {
407
            $bind = self::PARAM_FLOAT;
408
        } elseif (preg_match('/(int|serial|bit)/is', $type)) {
409
            $bind = PDO::PARAM_INT;
410
        } elseif (preg_match('/bool/is', $type)) {
411
            $bind = PDO::PARAM_BOOL;
412
        } else {
413
            $bind = PDO::PARAM_STR;
414
        }
415
416
        return $bind;
417
    }
418
419
    /**
420
     * 获取数据表信息
421
     * @access public
422
     * @param  mixed  $tableName 数据表名 留空自动获取
423
     * @param  string $fetch     获取信息类型 包括 fields type bind pk
424
     * @return mixed
425
     */
426
    public function getTableInfo($tableName, string $fetch = '')
427
    {
428
        if (is_array($tableName)) {
429
            $tableName = key($tableName) ?: current($tableName);
430
        }
431
432
        if (strpos($tableName, ',')) {
433
            // 多表不获取字段信息
434
            return [];
435
        }
436
437
        // 修正子查询作为表名的问题
438
        if (strpos($tableName, ')')) {
439
            return [];
440
        }
441
442
        list($tableName) = explode(' ', $tableName);
443
444
        if (!strpos($tableName, '.')) {
445
            $schema = $this->getConfig('database') . '.' . $tableName;
446
        } else {
447
            $schema = $tableName;
448
        }
449
450
        if (!isset(self::$info[$schema])) {
451
            // 读取缓存
452
            $cacheFile = Container::pull('app')->getRuntimePath() . 'schema' . DIRECTORY_SEPARATOR . $schema . '.php';
453
454
            if (!$this->config['debug'] && is_file($cacheFile)) {
455
                $info = include $cacheFile;
456
            } else {
457
                $info = $this->getFields($tableName);
458
            }
459
460
            $fields = array_keys($info);
461
            $bind   = $type   = [];
462
463
            foreach ($info as $key => $val) {
464
                // 记录字段类型
465
                $type[$key] = $val['type'];
466
                $bind[$key] = $this->getFieldBindType($val['type']);
467
468
                if (!empty($val['primary'])) {
469
                    $pk[] = $key;
470
                }
471
            }
472
473
            if (isset($pk)) {
474
                // 设置主键
475
                $pk = count($pk) > 1 ? $pk : $pk[0];
476
            } else {
477
                $pk = null;
478
            }
479
480
            self::$info[$schema] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
481
        }
482
483
        return $fetch ? self::$info[$schema][$fetch] : self::$info[$schema];
484
    }
485
486
    /**
487
     * 获取数据表的主键
488
     * @access public
489
     * @param  string $tableName 数据表名
490
     * @return string|array
491
     */
492
    public function getPk(string $tableName)
493
    {
494
        return $this->getTableInfo($tableName, 'pk');
495
    }
496
497
    /**
498
     * 获取数据表字段信息
499
     * @access public
500
     * @param  string $tableName 数据表名
501
     * @return array
502
     */
503
    public function getTableFields(string $tableName): array
504
    {
505
        return $this->getTableInfo($tableName, 'fields');
506
    }
507
508
    /**
509
     * 获取数据表字段类型
510
     * @access public
511
     * @param  string $tableName 数据表名
512
     * @param  string $field    字段名
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
513
     * @return array|string
514
     */
515
    public function getFieldsType(string $tableName, string $field = null)
516
    {
517
        $result = $this->getTableInfo($tableName, 'type');
518
519
        if ($field && isset($result[$field])) {
520
            return $result[$field];
521
        }
522
523
        return $result;
524
    }
525
526
    /**
527
     * 获取数据表绑定信息
528
     * @access public
529
     * @param  string $tableName 数据表名
530
     * @return array
531
     */
532
    public function getFieldsBind(string $tableName): array
533
    {
534
        return $this->getTableInfo($tableName, 'bind');
535
    }
536
537
    /**
538
     * 获取数据库的配置参数
539
     * @access public
540
     * @param  string $config 配置名称
541
     * @return mixed
542
     */
543
    public function getConfig(string $config = '')
544
    {
545
        if ('' === $config) {
546
            return $this->config;
547
        }
548
        return $this->config[$config] ?? null;
549
    }
550
551
    /**
552
     * 设置数据库的配置参数
553
     * @access public
554
     * @param  array $config 配置
555
     * @return void
556
     */
557
    public function setConfig(array $config): void
558
    {
559
        $this->config = array_merge($this->config, $config);
560
    }
561
562
    /**
563
     * 连接数据库方法
564
     * @access public
565
     * @param  array      $config 连接参数
0 ignored issues
show
Coding Style introduced by
Expected 9 spaces after parameter name; 1 found
Loading history...
566
     * @param  integer    $linkNum 连接序号
0 ignored issues
show
Coding Style introduced by
Expected 8 spaces after parameter name; 1 found
Loading history...
567
     * @param  array|bool $autoConnection 是否自动连接主数据库(用于分布式)
568
     * @return PDO
569
     * @throws Exception
570
     */
571
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
572
    {
573
        if (isset($this->links[$linkNum])) {
574
            return $this->links[$linkNum];
575
        }
576
577
        if (empty($config)) {
578
            $config = $this->config;
579
        } else {
580
            $config = array_merge($this->config, $config);
581
        }
582
583
        // 连接参数
584
        if (isset($config['params']) && is_array($config['params'])) {
585
            $params = $config['params'] + $this->params;
586
        } else {
587
            $params = $this->params;
588
        }
589
590
        // 记录当前字段属性大小写设置
591
        $this->attrCase = $params[PDO::ATTR_CASE];
592
593
        if (!empty($config['break_match_str'])) {
594
            $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
595
        }
596
597
        try {
598
            if (empty($config['dsn'])) {
599
                $config['dsn'] = $this->parseDsn($config);
600
            }
601
602
            if ($config['debug']) {
603
                $startTime             = microtime(true);
604
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
605
                // 记录数据库连接信息
606
                $this->log('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);
607
            } else {
608
                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
609
            }
610
611
            return $this->links[$linkNum];
612
        } catch (\PDOException $e) {
613
            if ($autoConnection) {
614
                $this->log($e->getMessage(), 'error');
615
                return $this->connect([], $linkNum);
616
            } else {
617
                throw $e;
618
            }
619
        }
620
    }
621
622
    /**
623
     * 释放查询结果
624
     * @access public
625
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
626
    public function free(): void
627
    {
628
        $this->PDOStatement = null;
629
    }
630
631
    /**
632
     * 获取PDO对象
633
     * @access public
634
     * @return \PDO|false
635
     */
636
    public function getPdo()
637
    {
638
        if (!$this->linkID) {
639
            return false;
640
        }
641
642
        return $this->linkID;
643
    }
644
645
    /**
646
     * 执行查询 使用生成器返回数据
647
     * @access public
648
     * @param  Query        $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
649
     * @param  string       $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
650
     * @param  array        $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
651
     * @param  \think\Model $model 模型对象实例
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
652
     * @param  array        $condition 查询条件
653
     * @return \Generator
654
     */
655
    public function getCursor(Query $query, string $sql, array $bind = [], $model = null, $condition = null)
656
    {
657
        $this->queryPDOStatement($query, $sql, $bind);
658
659
        // 返回结果集
660
        while ($result = $this->PDOStatement->fetch($this->fetchType)) {
661
            if ($model) {
662
                yield $model->newInstance($result, true, $condition);
663
            } else {
664
                yield $result;
665
            }
666
        }
667
    }
668
669
    /**
670
     * 执行查询 返回数据集
671
     * @access public
672
     * @param  Query  $query 查询对象
673
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
674
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
675
     * @param  bool   $cache 是否支持缓存
676
     * @return array
677
     * @throws BindParamException
678
     * @throws \PDOException
679
     * @throws \Exception
680
     * @throws \Throwable
681
     */
682
    public function query(Query $query, string $sql, array $bind = [], bool $cache = false): array
683
    {
684
        // 分析查询表达式
685
        $options = $query->parseOptions();
686
687
        if ($cache && !empty($options['cache'])) {
688
            $cacheItem = $this->parseCache($query, $options['cache']);
689
            $resultSet = $this->cache->get($cacheItem->getKey());
690
691
            if (false !== $resultSet) {
692
                return $resultSet;
693
            }
694
        }
695
696
        $master    = !empty($options['master']) ? true : false;
697
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
698
699
        $this->getPDOStatement($sql, $bind, $master, $procedure);
700
701
        $resultSet = $this->getResult($procedure);
702
703
        if (isset($cacheItem) && $resultSet) {
704
            // 缓存数据集
705
            $cacheItem->set($resultSet);
706
            $this->cacheData($cacheItem);
707
        }
708
709
        return $resultSet;
710
    }
711
712
    /**
713
     * 执行查询但只返回PDOStatement对象
714
     * @access public
715
     * @param  Query $query 查询对象
716
     * @return \PDOStatement
717
     */
718
    public function pdo(Query $query): PDOStatement
719
    {
720
        $bind = $query->getBind();
721
        // 生成查询SQL
722
        $sql = $this->builder->select($query);
723
724
        return $this->queryPDOStatement($query, $sql, $bind);
725
    }
726
727
    /**
728
     * 执行查询但只返回PDOStatement对象
729
     * @access public
730
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
731
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 6 spaces after parameter name; 1 found
Loading history...
732
     * @param  bool   $master 是否在主服务器读操作
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
733
     * @param  bool   $procedure 是否为存储过程调用
734
     * @return PDOStatement
735
     * @throws BindParamException
736
     * @throws \PDOException
737
     * @throws \Exception
738
     * @throws \Throwable
739
     */
740
    public function getPDOStatement(string $sql, array $bind = [], bool $master = false, bool $procedure = false): PDOStatement
741
    {
742
        $this->initConnect($master);
743
744
        // 记录SQL语句
745
        $this->queryStr = $sql;
746
747
        $this->bind = $bind;
748
749
        Db::$queryTimes++;
750
751
        try {
752
            // 调试开始
753
            $this->debug(true);
754
755
            // 预处理
756
            $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...
757
758
            // 参数绑定
759
            if ($procedure) {
760
                $this->bindParam($bind);
761
            } else {
762
                $this->bindValue($bind);
763
            }
764
765
            // 执行查询
766
            $this->PDOStatement->execute();
767
768
            // 调试结束
769
            $this->debug(false, '', $master);
770
771
            return $this->PDOStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->PDOStatement could return the type boolean which is incompatible with the type-hinted return PDOStatement. Consider adding an additional type-check to rule them out.
Loading history...
772
        } catch (\Throwable | \Exception $e) {
773
            if ($this->isBreak($e)) {
774
                return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
775
            }
776
777
            if ($e instanceof \PDOException) {
778
                throw new PDOException($e, $this->config, $this->getLastsql());
779
            } else {
780
                throw $e;
781
            }
782
        }
783
    }
784
785
    /**
786
     * 执行语句
787
     * @access public
788
     * @param  Query  $query 查询对象
789
     * @param  string $sql sql指令
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
790
     * @param  array  $bind 参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
791
     * @return int
792
     * @throws BindParamException
793
     * @throws \PDOException
794
     * @throws \Exception
795
     * @throws \Throwable
796
     */
797
    public function execute(Query $query, string $sql, array $bind = []): int
798
    {
799
        $this->queryPDOStatement($query->master(true), $sql, $bind);
800
801
        $this->numRows = $this->PDOStatement->rowCount();
802
803
        return $this->numRows;
804
    }
805
806
    protected function queryPDOStatement(Query $query, string $sql, array $bind = []): PDOStatement
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
807
    {
808
        $options   = $query->parseOptions();
809
        $master    = !empty($options['master']) ? true : false;
810
        $procedure = !empty($options['procedure']) ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
811
812
        return $this->getPDOStatement($sql, $bind, $master, $procedure);
813
    }
814
815
    /**
816
     * 查找单条记录
817
     * @access public
818
     * @param  Query $query 查询对象
819
     * @return array
820
     * @throws DbException
821
     * @throws ModelNotFoundException
822
     * @throws DataNotFoundException
823
     */
824
    public function find(Query $query): array
825
    {
826
        // 分析查询表达式
827
        $options = $query->parseOptions();
828
829
        if (!empty($options['cache'])) {
830
            // 判断查询缓存
831
            $cacheItem = $this->parseCache($query, $options['cache']);
832
            $key       = $cacheItem->getKey();
833
        }
834
835
        if (isset($key)) {
836
            $result = $this->cache->get($key);
837
838
            if (false !== $result) {
839
                return $result;
840
            }
841
        }
842
843
        // 生成查询SQL
844
        $sql = $this->builder->select($query, true);
845
846
        // 事件回调
847
        $result = $query->trigger('before_find');
848
849
        if (!$result) {
850
            // 执行查询
851
            $resultSet = $this->query($query, $sql, $query->getBind());
852
853
            $result = $resultSet[0] ?? [];
854
        }
855
856
        if (isset($cacheItem) && $result) {
857
            // 缓存数据
858
            $cacheItem->set($result);
859
            $this->cacheData($cacheItem);
860
        }
861
862
        return $result;
863
    }
864
865
    /**
866
     * 使用游标查询记录
867
     * @access public
868
     * @param  Query $query 查询对象
869
     * @return \Generator
870
     */
871
    public function cursor(Query $query)
872
    {
873
        // 分析查询表达式
874
        $options = $query->parseOptions();
875
876
        // 生成查询SQL
877
        $sql = $this->builder->select($query);
878
879
        $condition = $options['where']['AND'] ?? null;
880
881
        // 执行查询操作
882
        return $this->getCursor($query, $sql, $query->getBind(), $query->getModel(), $condition);
883
    }
884
885
    /**
886
     * 查找记录
887
     * @access public
888
     * @param  Query $query 查询对象
889
     * @return array
890
     * @throws DbException
891
     * @throws ModelNotFoundException
892
     * @throws DataNotFoundException
893
     */
894
    public function select(Query $query): array
895
    {
896
        // 分析查询表达式
897
        $options = $query->parseOptions();
898
899
        if (!empty($options['cache'])) {
900
            $cacheItem = $this->parseCache($query, $options['cache']);
901
            $resultSet = $this->getCacheData($cacheItem);
902
903
            if (false !== $resultSet) {
904
                return $resultSet;
905
            }
906
        }
907
908
        // 生成查询SQL
909
        $sql = $this->builder->select($query);
910
911
        $resultSet = $query->trigger('before_select');
912
913
        if (!$resultSet) {
914
            // 执行查询操作
915
            $resultSet = $this->query($query, $sql, $query->getBind());
916
        }
917
918
        if (isset($cacheItem) && false !== $resultSet) {
919
            // 缓存数据集
920
            $cacheItem->set($resultSet);
921
            $this->cacheData($cacheItem);
922
        }
923
924
        return $resultSet;
925
    }
926
927
    /**
928
     * 插入记录
929
     * @access public
930
     * @param  Query   $query        查询对象
931
     * @param  boolean $replace      是否replace
932
     * @param  boolean $getLastInsID 返回自增主键
933
     * @param  string  $sequence     自增序列名
934
     * @return mixed
935
     */
936
    public function insert(Query $query, bool $replace = false, bool $getLastInsID = false, string $sequence = null)
937
    {
938
        // 分析查询表达式
939
        $options = $query->parseOptions();
940
941
        // 生成SQL语句
942
        $sql = $this->builder->insert($query, $replace);
943
944
        // 执行操作
945
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
946
947
        if ($result) {
948
            $sequence  = $sequence ?: ($options['sequence'] ?? null);
949
            $lastInsId = $this->getLastInsID($sequence);
950
951
            $data = $options['data'];
952
953
            if ($lastInsId) {
954
                $pk = $query->getPk();
955
                if (is_string($pk)) {
956
                    $data[$pk] = $lastInsId;
957
                }
958
            }
959
960
            $query->setOption('data', $data);
961
962
            $query->trigger('after_insert');
963
964
            if ($getLastInsID) {
965
                return $lastInsId;
966
            }
967
        }
968
969
        return $result;
970
    }
971
972
    /**
973
     * 批量插入记录
974
     * @access public
975
     * @param  Query   $query   查询对象
976
     * @param  mixed   $dataSet 数据集
977
     * @param  bool    $replace 是否replace
978
     * @param  integer $limit   每次写入数据限制
979
     * @return integer
980
     * @throws \Exception
981
     * @throws \Throwable
982
     */
983
    public function insertAll(Query $query, array $dataSet = [], bool $replace = false, int $limit = 0): int
984
    {
985
        if (!is_array(reset($dataSet))) {
986
            return 0;
987
        }
988
989
        $query->parseOptions();
990
991
        if ($limit) {
992
            // 分批写入 自动启动事务支持
993
            $this->startTrans();
994
995
            try {
996
                $array = array_chunk($dataSet, $limit, true);
997
                $count = 0;
998
999
                foreach ($array as $item) {
1000
                    $sql = $this->builder->insertAll($query, $item, $replace);
1001
                    $count += $this->execute($query, $sql, $query->getBind());
1002
                }
1003
1004
                // 提交事务
1005
                $this->commit();
1006
            } catch (\Exception | \Throwable $e) {
1007
                $this->rollback();
1008
                throw $e;
1009
            }
1010
1011
            return $count;
1012
        }
1013
1014
        $sql = $this->builder->insertAll($query, $dataSet, $replace);
1015
1016
        return $this->execute($query, $sql, $query->getBind());
1017
    }
1018
1019
    /**
1020
     * 通过Select方式插入记录
1021
     * @access public
1022
     * @param  Query  $query  查询对象
1023
     * @param  array  $fields 要插入的数据表字段名
1024
     * @param  string $table  要插入的数据表名
1025
     * @return integer
1026
     * @throws PDOException
1027
     */
1028
    public function selectInsert(Query $query, array $fields, string $table): int
1029
    {
1030
        // 分析查询表达式
1031
        $sql = $this->builder->selectInsert($query, $fields, $table);
1032
1033
        return $this->execute($query, $sql, $query->getBind());
1034
    }
1035
1036
    /**
1037
     * 更新记录
1038
     * @access public
1039
     * @param  Query $query 查询对象
1040
     * @return integer
1041
     * @throws Exception
1042
     * @throws PDOException
1043
     */
1044
    public function update(Query $query): int
1045
    {
1046
        $options = $query->parseOptions();
1047
1048
        if (isset($options['cache'])) {
1049
            $cacheItem = $this->parseCache($query, $options['cache']);
1050
            $key       = $cacheItem->getKey();
1051
        }
1052
1053
        // 生成UPDATE SQL语句
1054
        $sql = $this->builder->update($query);
1055
1056
        // 检测缓存
1057
        if (isset($key) && $this->cache->get($key)) {
1058
            // 删除缓存
1059
            $this->cache->delete($key);
1060
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1061
            $this->cache->tag($cacheItem->getTag())->clear();
1062
        }
1063
1064
        // 执行操作
1065
        $result = '' == $sql ? 0 : $this->execute($query, $sql, $query->getBind());
1066
1067
        if ($result) {
1068
            $query->trigger('after_update');
1069
        }
1070
1071
        return $result;
1072
    }
1073
1074
    /**
1075
     * 删除记录
1076
     * @access public
1077
     * @param  Query $query 查询对象
1078
     * @return int
1079
     * @throws Exception
1080
     * @throws PDOException
1081
     */
1082
    public function delete(Query $query): int
1083
    {
1084
        // 分析查询表达式
1085
        $options = $query->parseOptions();
1086
1087
        if (isset($options['cache'])) {
1088
            $cacheItem = $this->parseCache($query, $options['cache']);
1089
            $key       = $cacheItem->getKey();
1090
        }
1091
1092
        // 生成删除SQL语句
1093
        $sql = $this->builder->delete($query);
1094
1095
        // 检测缓存
1096
        if (isset($key) && $this->cache->get($key)) {
1097
            // 删除缓存
1098
            $this->cache->delete($key);
1099
        } elseif (isset($cacheItem) && $cacheItem->getTag()) {
1100
            $this->cache->tag($cacheItem->getTag())->clear();
1101
        }
1102
1103
        // 执行操作
1104
        $result = $this->execute($query, $sql, $query->getBind());
1105
1106
        if ($result) {
1107
            $query->trigger('after_delete');
1108
        }
1109
1110
        return $result;
1111
    }
1112
1113
    /**
1114
     * 得到某个字段的值
1115
     * @access public
1116
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Expected 3 spaces after parameter name; 1 found
Loading history...
1117
     * @param  string $field   字段名
1118
     * @param  mixed  $default   默认值
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1119
     * @return mixed
1120
     */
1121
    public function value(Query $query, string $field, $default = null)
1122
    {
1123
        $options = $query->parseOptions();
1124
1125
        if (isset($options['field'])) {
1126
            $query->removeOption('field');
1127
        }
1128
1129
        $query->setOption('field', (array) $field);
1130
1131
        if (!empty($options['cache'])) {
1132
            $cacheItem = $this->parseCache($query, $options['cache']);
1133
            $result    = $this->getCacheData($cacheItem);
1134
1135
            if (false !== $result) {
1136
                return $result;
1137
            }
1138
        }
1139
1140
        // 生成查询SQL
1141
        $sql = $this->builder->select($query, true);
1142
1143
        if (isset($options['field'])) {
1144
            $query->setOption('field', $options['field']);
1145
        } else {
1146
            $query->removeOption('field');
1147
        }
1148
1149
        // 执行查询操作
1150
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1151
1152
        $result = $pdo->fetchColumn();
1153
1154
        if (isset($cacheItem) && false !== $result) {
1155
            // 缓存数据
1156
            $cacheItem->set($result);
1157
            $this->cacheData($cacheItem);
1158
        }
1159
1160
        return false !== $result ? $result : $default;
1161
    }
1162
1163
    /**
1164
     * 得到某个字段的值
1165
     * @access public
1166
     * @param  Query  $query     查询对象
1167
     * @param  string $aggregate 聚合方法
1168
     * @param  mixed  $field     字段名
1169
     * @param  bool   $force     强制转为数字类型
1170
     * @return mixed
1171
     */
1172
    public function aggregate(Query $query, string $aggregate, $field, bool $force = false)
1173
    {
1174
        if (is_string($field) && 0 === stripos($field, 'DISTINCT ')) {
1175
            list($distinct, $field) = explode(' ', $field);
1176
        }
1177
1178
        $field = $aggregate . '(' . (!empty($distinct) ? 'DISTINCT ' : '') . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);
1179
1180
        $result = $this->value($query, $field, 0);
1181
1182
        return $force ? (float) $result : $result;
1183
    }
1184
1185
    /**
1186
     * 得到某个列的数组
1187
     * @access public
1188
     * @param  Query  $query 查询对象
1189
     * @param  string $field 字段名 多个字段用逗号分隔
1190
     * @param  string $key   索引
1191
     * @return array
1192
     */
1193
    public function column(Query $query, string $field, string $key = ''): array
1194
    {
1195
        $options = $query->parseOptions();
1196
1197
        if (isset($options['field'])) {
1198
            $query->removeOption('field');
1199
        }
1200
1201
        if ($key && '*' != $field) {
1202
            $field = $key . ',' . $field;
1203
        }
1204
1205
        $field = array_map('trim', explode(',', $field));
1206
1207
        $query->setOption('field', $field);
1208
1209
        if (!empty($options['cache'])) {
1210
            // 判断查询缓存
1211
            $cacheItem = $this->parseCache($query, $options['cache']);
1212
            $result    = $this->getCacheData($cacheItem);
1213
1214
            if (false !== $result) {
1215
                return $result;
1216
            }
1217
        }
1218
1219
        // 生成查询SQL
1220
        $sql = $this->builder->select($query);
1221
1222
        if (isset($options['field'])) {
1223
            $query->setOption('field', $options['field']);
1224
        } else {
1225
            $query->removeOption('field');
1226
        }
1227
1228
        // 执行查询操作
1229
        $pdo = $this->getPDOStatement($sql, $query->getBind(), $options['master']);
1230
1231
        if (1 == $pdo->columnCount()) {
1232
            $result = $pdo->fetchAll(PDO::FETCH_COLUMN);
1233
        } else {
1234
            $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
1235
1236
            if ('*' == $field && $key) {
1237
                $result = array_column($resultSet, null, $key);
1238
            } elseif (!empty($resultSet)) {
1239
                $fields = array_keys($resultSet[0]);
1240
                $count  = count($fields);
1241
                $key1   = array_shift($fields);
1242
                $key2   = $fields ? array_shift($fields) : '';
1243
                $key    = $key ?: $key1;
1244
1245
                if (strpos($key, '.')) {
1246
                    list($alias, $key) = explode('.', $key);
1247
                }
1248
1249
                if (2 == $count) {
1250
                    $column = $key2;
1251
                } elseif (1 == $count) {
1252
                    $column = $key1;
1253
                } else {
1254
                    $column = null;
1255
                }
1256
1257
                $result = array_column($resultSet, $column, $key);
1258
            } else {
1259
                $result = [];
1260
            }
1261
        }
1262
1263
        if (isset($cacheItem)) {
1264
            // 缓存数据
1265
            $cacheItem->set($result);
1266
            $this->cacheData($cacheItem);
1267
        }
1268
1269
        return $result;
1270
    }
1271
1272
    /**
1273
     * 根据参数绑定组装最终的SQL语句 便于调试
1274
     * @access public
1275
     * @param  string $sql 带参数绑定的sql语句
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1276
     * @param  array  $bind 参数绑定列表
1277
     * @return string
1278
     */
1279
    public function getRealSql(string $sql, array $bind = []): string
1280
    {
1281
        foreach ($bind as $key => $val) {
1282
            $value = is_array($val) ? $val[0] : $val;
1283
            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
1284
1285
            if (self::PARAM_FLOAT == $type) {
1286
                $value = (float) $value;
1287
            } elseif (PDO::PARAM_STR == $type && is_string($value)) {
1288
                $value = '\'' . addslashes($value) . '\'';
1289
            } elseif (PDO::PARAM_INT == $type && '' === $value) {
1290
                $value = 0;
1291
            }
1292
1293
            // 判断占位符
1294
            $sql = is_numeric($key) ?
1295
            substr_replace($sql, $value, strpos($sql, '?'), 1) :
1296
            substr_replace($sql, $value, strpos($sql, ':' . $key), strlen(':' . $key));
1297
        }
1298
1299
        return rtrim($sql);
1300
    }
1301
1302
    /**
1303
     * 参数绑定
1304
     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
1305
     * 或者 ['value',123] 对应问号占位符
1306
     * @access public
1307
     * @param  array $bind 要绑定的参数列表
1308
     * @return void
1309
     * @throws BindParamException
1310
     */
1311
    protected function bindValue(array $bind = []): void
1312
    {
1313
        foreach ($bind as $key => $val) {
1314
            // 占位符
1315
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1316
1317
            if (is_array($val)) {
1318
                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
1319
                    $val[0] = 0;
1320
                } elseif (self::PARAM_FLOAT == $val[1]) {
1321
                    $val[0] = (float) $val[0];
1322
                    $val[1] = PDO::PARAM_STR;
1323
                }
1324
1325
                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
1326
            } else {
1327
                $result = $this->PDOStatement->bindValue($param, $val);
1328
            }
1329
1330
            if (!$result) {
1331
                throw new BindParamException(
1332
                    "Error occurred  when binding parameters '{$param}'",
1333
                    $this->config,
1334
                    $this->getLastsql(),
1335
                    $bind
1336
                );
1337
            }
1338
        }
1339
    }
1340
1341
    /**
1342
     * 存储过程的输入输出参数绑定
1343
     * @access public
1344
     * @param  array $bind 要绑定的参数列表
1345
     * @return void
1346
     * @throws BindParamException
1347
     */
1348
    protected function bindParam(array $bind): void
1349
    {
1350
        foreach ($bind as $key => $val) {
1351
            $param = is_numeric($key) ? $key + 1 : ':' . $key;
1352
1353
            if (is_array($val)) {
1354
                array_unshift($val, $param);
1355
                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
1356
            } else {
1357
                $result = $this->PDOStatement->bindValue($param, $val);
1358
            }
1359
1360
            if (!$result) {
1361
                $param = array_shift($val);
1362
1363
                throw new BindParamException(
1364
                    "Error occurred  when binding parameters '{$param}'",
1365
                    $this->config,
1366
                    $this->getLastsql(),
1367
                    $bind
1368
                );
1369
            }
1370
        }
1371
    }
1372
1373
    /**
1374
     * 获得数据集数组
1375
     * @access protected
1376
     * @param  bool $procedure 是否存储过程
1377
     * @return array
1378
     */
1379
    protected function getResult(bool $procedure = false): array
1380
    {
1381
        if ($procedure) {
1382
            // 存储过程返回结果
1383
            return $this->procedure();
1384
        }
1385
1386
        $result = $this->PDOStatement->fetchAll($this->fetchType);
1387
1388
        $this->numRows = count($result);
1389
1390
        return $result;
1391
    }
1392
1393
    /**
1394
     * 获得存储过程数据集
1395
     * @access protected
1396
     * @return array
1397
     */
1398
    protected function procedure(): array
1399
    {
1400
        $item = [];
1401
1402
        do {
1403
            $result = $this->getResult();
1404
            if (!empty($result)) {
1405
                $item[] = $result;
1406
            }
1407
        } while ($this->PDOStatement->nextRowset());
1408
1409
        $this->numRows = count($item);
1410
1411
        return $item;
1412
    }
1413
1414
    /**
1415
     * 执行数据库事务
1416
     * @access public
1417
     * @param  callable $callback 数据操作方法回调
1418
     * @return mixed
1419
     * @throws PDOException
1420
     * @throws \Exception
1421
     * @throws \Throwable
1422
     */
1423
    public function transaction(callable $callback)
1424
    {
1425
        $this->startTrans();
1426
1427
        try {
1428
            $result = null;
1429
            if (is_callable($callback)) {
1430
                $result = call_user_func_array($callback, [$this]);
1431
            }
1432
1433
            $this->commit();
1434
            return $result;
1435
        } catch (\Exception | \Throwable $e) {
1436
            $this->rollback();
1437
            throw $e;
1438
        }
1439
    }
1440
1441
    /**
1442
     * 启动事务
1443
     * @access public
1444
     * @return void
1445
     * @throws \PDOException
1446
     * @throws \Exception
1447
     */
1448
    public function startTrans(): void
1449
    {
1450
        $this->initConnect(true);
1451
1452
        ++$this->transTimes;
1453
1454
        try {
1455
            if (1 == $this->transTimes) {
1456
                $this->linkID->beginTransaction();
1457
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1458
                $this->linkID->exec(
1459
                    $this->parseSavepoint('trans' . $this->transTimes)
1460
                );
1461
            }
1462
        } catch (\Exception $e) {
1463
            if ($this->isBreak($e)) {
1464
                --$this->transTimes;
1465
                $this->close()->startTrans();
1466
            }
1467
            throw $e;
1468
        }
1469
    }
1470
1471
    /**
1472
     * 用于非自动提交状态下面的查询提交
1473
     * @access public
1474
     * @return void
1475
     * @throws PDOException
1476
     */
1477
    public function commit(): void
1478
    {
1479
        $this->initConnect(true);
1480
1481
        if (1 == $this->transTimes) {
1482
            $this->linkID->commit();
1483
        }
1484
1485
        --$this->transTimes;
1486
    }
1487
1488
    /**
1489
     * 事务回滚
1490
     * @access public
1491
     * @return void
1492
     * @throws PDOException
1493
     */
1494
    public function rollback(): void
1495
    {
1496
        $this->initConnect(true);
1497
1498
        if (1 == $this->transTimes) {
1499
            $this->linkID->rollBack();
1500
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
1501
            $this->linkID->exec(
1502
                $this->parseSavepointRollBack('trans' . $this->transTimes)
1503
            );
1504
        }
1505
1506
        $this->transTimes = max(0, $this->transTimes - 1);
1507
    }
1508
1509
    /**
1510
     * 是否支持事务嵌套
1511
     * @return bool
1512
     */
1513
    protected function supportSavepoint(): bool
1514
    {
1515
        return false;
1516
    }
1517
1518
    /**
1519
     * 生成定义保存点的SQL
1520
     * @access protected
1521
     * @param  string $name 标识
1522
     * @return string
1523
     */
1524
    protected function parseSavepoint(string $name): string
1525
    {
1526
        return 'SAVEPOINT ' . $name;
1527
    }
1528
1529
    /**
1530
     * 生成回滚到保存点的SQL
1531
     * @access protected
1532
     * @param  string $name 标识
1533
     * @return string
1534
     */
1535
    protected function parseSavepointRollBack(string $name): string
1536
    {
1537
        return 'ROLLBACK TO SAVEPOINT ' . $name;
1538
    }
1539
1540
    /**
1541
     * 批处理执行SQL语句
1542
     * 批处理的指令都认为是execute操作
1543
     * @access public
1544
     * @param  Query $query        查询对象
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 8 found
Loading history...
1545
     * @param  array $sqlArray   SQL批处理指令
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 3 found
Loading history...
1546
     * @param  array $bind       参数绑定
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 7 found
Loading history...
1547
     * @return bool
1548
     */
1549
    public function batchQuery(Query $query, array $sqlArray = [], array $bind = []): bool
1550
    {
1551
        // 自动启动事务支持
1552
        $this->startTrans();
1553
1554
        try {
1555
            foreach ($sqlArray as $sql) {
1556
                $this->execute($query, $sql, $bind);
1557
            }
1558
            // 提交事务
1559
            $this->commit();
1560
        } catch (\Exception $e) {
1561
            $this->rollback();
1562
            throw $e;
1563
        }
1564
1565
        return true;
1566
    }
1567
1568
    /**
1569
     * 关闭数据库(或者重新连接)
1570
     * @access public
1571
     * @return $this
1572
     */
1573
    public function close()
1574
    {
1575
        $this->linkID    = null;
1576
        $this->linkWrite = null;
1577
        $this->linkRead  = null;
1578
        $this->links     = [];
1579
1580
        $this->free();
1581
1582
        return $this;
1583
    }
1584
1585
    /**
1586
     * 是否断线
1587
     * @access protected
1588
     * @param  \PDOException|\Exception $e 异常对象
1589
     * @return bool
1590
     */
1591
    protected function isBreak($e): bool
1592
    {
1593
        if (!$this->config['break_reconnect']) {
1594
            return false;
1595
        }
1596
1597
        $error = $e->getMessage();
1598
1599
        foreach ($this->breakMatchStr as $msg) {
1600
            if (false !== stripos($error, $msg)) {
1601
                return true;
1602
            }
1603
        }
1604
1605
        return false;
1606
    }
1607
1608
    /**
1609
     * 获取最近一次查询的sql语句
1610
     * @access public
1611
     * @return string
1612
     */
1613
    public function getLastSql(): string
1614
    {
1615
        return $this->getRealSql($this->queryStr, $this->bind);
1616
    }
1617
1618
    /**
1619
     * 获取最近插入的ID
1620
     * @access public
1621
     * @param  string $sequence 自增序列名
1622
     * @return string
1623
     */
1624
    public function getLastInsID(string $sequence = null): string
1625
    {
1626
        return $this->linkID->lastInsertId($sequence);
1627
    }
1628
1629
    /**
1630
     * 获取返回或者影响的记录数
1631
     * @access public
1632
     * @return integer
1633
     */
1634
    public function getNumRows(): int
1635
    {
1636
        return $this->numRows;
1637
    }
1638
1639
    /**
1640
     * 获取最近的错误信息
1641
     * @access public
1642
     * @return string
1643
     */
1644
    public function getError(): string
1645
    {
1646
        if ($this->PDOStatement) {
1647
            $error = $this->PDOStatement->errorInfo();
1648
            $error = $error[1] . ':' . $error[2];
1649
        } else {
1650
            $error = '';
1651
        }
1652
1653
        if ('' != $this->queryStr) {
1654
            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
1655
        }
1656
1657
        return $error;
1658
    }
1659
1660
    /**
1661
     * 数据库调试 记录当前SQL及分析性能
1662
     * @access protected
1663
     * @param  boolean $start 调试开始标记 true 开始 false 结束
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1664
     * @param  string  $sql 执行的SQL语句 留空自动获取
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
1665
     * @param  bool    $master 主从标记
1666
     * @return void
1667
     */
1668
    protected function debug(bool $start, string $sql = '', bool $master = false): void
1669
    {
1670
        if (!empty($this->config['debug'])) {
1671
            // 开启数据库调试模式
1672
            $debug = Container::pull('debug');
1673
1674
            if ($start) {
1675
                $debug->remark('queryStartTime', 'time');
1676
            } else {
1677
                // 记录操作结束时间
1678
                $debug->remark('queryEndTime', 'time');
1679
                $runtime = $debug->getRangeTime('queryStartTime', 'queryEndTime');
1680
                $sql     = $sql ?: $this->getLastsql();
1681
                $result  = [];
1682
1683
                // SQL性能分析
1684
                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
1685
                    $result = $this->getExplain($sql);
1686
                }
1687
1688
                // SQL监听
1689
                $this->triggerSql($sql, $runtime, $result, $master);
1690
            }
1691
        }
1692
    }
1693
1694
    /**
1695
     * 监听SQL执行
1696
     * @access public
1697
     * @param  callable $callback 回调方法
1698
     * @return void
1699
     */
1700
    public function listen(callable $callback): void
1701
    {
1702
        self::$event[] = $callback;
1703
    }
1704
1705
    /**
1706
     * 触发SQL事件
1707
     * @access protected
1708
     * @param  string $sql SQL语句
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 1 found
Loading history...
1709
     * @param  string $runtime SQL运行时间
1710
     * @param  mixed  $explain SQL分析
1711
     * @param  bool   $master 主从标记
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
1712
     * @return void
1713
     */
1714
    protected function triggerSql(string $sql, string $runtime, array $explain = [], bool $master = false): void
1715
    {
1716
        if (!empty(self::$event)) {
1717
            foreach (self::$event as $callback) {
1718
                if (is_callable($callback)) {
1719
                    call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
1720
                }
1721
            }
1722
        } else {
1723
            if ($this->config['deploy']) {
1724
                // 分布式记录当前操作的主从
1725
                $master = $master ? 'master|' : 'slave|';
1726
            } else {
1727
                $master = '';
1728
            }
1729
1730
            // 未注册监听则记录到日志中
1731
            $this->log('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]');
1732
1733
            if (!empty($explain)) {
1734
                $this->log('[ EXPLAIN : ' . var_export($explain, true) . ' ]');
1735
            }
1736
        }
1737
    }
1738
1739
    public function log($log, string $type = 'sql'): void
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
1740
    {
1741
        $this->config['debug'] && Container::pull('log')->record($log, $type);
1742
    }
1743
1744
    /**
1745
     * 初始化数据库连接
1746
     * @access protected
1747
     * @param  boolean $master 是否主服务器
1748
     * @return void
1749
     */
1750
    protected function initConnect(bool $master = true): void
1751
    {
1752
        if (!empty($this->config['deploy'])) {
1753
            // 采用分布式数据库
1754
            if ($master || $this->transTimes) {
1755
                if (!$this->linkWrite) {
1756
                    $this->linkWrite = $this->multiConnect(true);
1757
                }
1758
1759
                $this->linkID = $this->linkWrite;
1760
            } else {
1761
                if (!$this->linkRead) {
1762
                    $this->linkRead = $this->multiConnect(false);
1763
                }
1764
1765
                $this->linkID = $this->linkRead;
1766
            }
1767
        } elseif (!$this->linkID) {
1768
            // 默认单数据库
1769
            $this->linkID = $this->connect();
1770
        }
1771
    }
1772
1773
    /**
1774
     * 连接分布式服务器
1775
     * @access protected
1776
     * @param  boolean $master 主服务器
1777
     * @return PDO
1778
     */
1779
    protected function multiConnect(bool $master = false): PDO
1780
    {
1781
        $_config = [];
1782
1783
        // 分布式数据库配置解析
1784
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1785
            $_config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
1786
        }
1787
1788
        // 主服务器序号
1789
        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
1790
1791
        if ($this->config['rw_separate']) {
1792
            // 主从式采用读写分离
1793
            if ($master) // 主服务器写入
0 ignored issues
show
Coding Style introduced by
Expected "if (...) {\n"; found "if (...) // 主服务器写入\n {\n"
Loading history...
1794
            {
1795
                $r = $m;
1796
            } elseif (is_numeric($this->config['slave_no'])) {
1797
                // 指定服务器读
1798
                $r = $this->config['slave_no'];
1799
            } else {
1800
                // 读操作连接从服务器 每次随机连接的数据库
1801
                $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
1802
            }
1803
        } else {
1804
            // 读写操作不区分服务器 每次随机连接的数据库
1805
            $r = floor(mt_rand(0, count($_config['hostname']) - 1));
1806
        }
1807
        $dbMaster = false;
1808
1809
        if ($m != $r) {
1810
            $dbMaster = [];
1811
            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1812
                $dbMaster[$name] = $_config[$name][$m] ?? $_config[$name][0];
1813
            }
1814
        }
1815
1816
        $dbConfig = [];
1817
1818
        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
1819
            $dbConfig[$name] = $_config[$name][$r] ?? $_config[$name][0];
1820
        }
1821
1822
        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
1823
    }
1824
1825
    /**
1826
     * 析构方法
1827
     * @access public
1828
     */
1829
    public function __destruct()
1830
    {
1831
        // 释放查询
1832
        $this->free();
1833
1834
        // 关闭连接
1835
        $this->close();
1836
    }
1837
1838
    /**
1839
     * 缓存数据
1840
     * @access protected
1841
     * @param  CacheItem $cacheItem 缓存Item
1842
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1843
    protected function cacheData(CacheItem $cacheItem): void
1844
    {
1845
        if ($cacheItem->getTag()) {
1846
            $this->cache->tag($cacheItem->getTag());
1847
        }
1848
1849
        $this->cache->set($cacheItem->getKey(), $cacheItem->get(), $cacheItem->getExpire());
1850
    }
1851
1852
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $cacheItem should have a doc-comment as per coding-style.
Loading history...
1853
     * 获取缓存数据
1854
     * @access protected
1855
     * @param  Query  $query 查询对象
0 ignored issues
show
Coding Style introduced by
Doc comment for parameter $query does not match actual variable name $cacheItem
Loading history...
1856
     * @param  mixed  $cache 缓存设置
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1857
     * @param  array  $data  缓存数据
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1858
     * @param  string $key   缓存Key
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
1859
     * @return mixed
1860
     */
1861
    protected function getCacheData(CacheItem $cacheItem)
1862
    {
1863
        // 判断查询缓存
1864
        return $this->cache->get($cacheItem->getKey());
1865
    }
1866
1867
    protected function parseCache(Query $query, array $cache): CacheItem
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
1868
    {
1869
        list($key, $expire, $tag) = $cache;
1870
1871
        if ($key instanceof CacheItem) {
1872
            $cacheItem = $key;
1873
        } else {
1874
            if (true === $key) {
1875
                if (!empty($query->getOptions('key'))) {
1876
                    $key = 'think:' . $this->getConfig('database') . '.' . $query->getTable() . '|' . $query->getOptions('key');
1877
                } else {
1878
                    $key = md5($this->getConfig('database') . serialize($query->getOptions()) . serialize($query->getBind(false)));
1879
                }
1880
            }
1881
1882
            $cacheItem = new CacheItem($key);
1883
            $cacheItem->expire($expire);
1884
            $cacheItem->tag($tag);
1885
        }
1886
1887
        return $cacheItem;
1888
    }
1889
1890
    /**
1891
     * 延时更新检查 返回false表示需要延时
1892
     * 否则返回实际写入的数值
1893
     * @access public
1894
     * @param  string  $type     自增或者自减
1895
     * @param  string  $guid     写入标识
1896
     * @param  integer $step     写入步进值
1897
     * @param  integer $lazyTime 延时时间(s)
1898
     * @return false|integer
1899
     */
1900
    public function lazyWrite(string $type, string $guid, int $step, int $lazyTime)
1901
    {
1902
        if (!$this->cache->has($guid . '_time')) {
1903
            // 计时开始
1904
            $this->cache->set($guid . '_time', time(), 0);
1905
            $this->cache->$type($guid, $step);
1906
        } elseif (time() > $this->cache->get($guid . '_time') + $lazyTime) {
1907
            // 删除缓存
1908
            $value = $this->cache->$type($guid, $step);
1909
            $this->cache->delete($guid);
1910
            $this->cache->delete($guid . '_time');
1911
            return 0 === $value ? false : $value;
1912
        } else {
1913
            // 更新缓存
1914
            $this->cache->$type($guid, $step);
1915
        }
1916
1917
        return false;
1918
    }
1919
}
1920