├── .gitignore
├── tests
├── autoload.php
├── demo.php
├── DbTest2.php
└── DbTest.php
├── src
├── Exception.php
├── NotFoundException.php
├── Expression.php
├── Relations
│ ├── RelationBase.php
│ ├── BelongsTo.php
│ ├── HasOne.php
│ └── HasMany.php
├── ClassLoader.php
├── DataProvider.php
├── ModelTrait.php
├── Pagination.php
├── Connection.php
└── Builder.php
├── composer.json
├── phpunit.xml
├── README.md
└── doc
└── index.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /vendor
3 | /.idea
4 | /composer.lock
5 |
--------------------------------------------------------------------------------
/tests/autoload.php:
--------------------------------------------------------------------------------
1 | register();
--------------------------------------------------------------------------------
/src/Exception.php:
--------------------------------------------------------------------------------
1 | value = $value;
12 | }
13 |
14 | public function getValue()
15 | {
16 | return $this->value;
17 | }
18 |
19 | public function __toString()
20 | {
21 | return (string)$this->getValue();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pfinal/database",
3 | "description": "PHP MySQL database wrapper, extends PDO and PDOStatement",
4 | "license": "MIT",
5 | "homepage": "http://www.pfinal.cn",
6 | "authors": [
7 | {
8 | "name": "Zou Yiliang"
9 | }
10 | ],
11 | "require": {
12 | "php": ">=5.4"
13 | },
14 | "require-dev": {
15 | "phpunit/phpunit": "^5.0",
16 | "symfony/var-dumper": "*"
17 | },
18 | "autoload": {
19 | "psr-4": {
20 | "PFinal\\Database\\": "src/"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Relations/RelationBase.php:
--------------------------------------------------------------------------------
1 | 0) {
12 | foreach ($relations as $relation) {
13 |
14 | $ind = strpos($relation, '.');
15 | if ($ind !== false) {
16 | $methodName = substr($relation, 0, $ind);
17 | $nextMethodName = substr($relation, $ind + 1);
18 | } else {
19 | $methodName = $relation;
20 | $nextMethodName = [];
21 | }
22 |
23 | $relationObj = call_user_func([$models[0], $methodName]);
24 | $relationObj->appendData($models, $methodName, $nextMethodName);
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Database](http://pfinal.cn)
2 |
3 | 数据库操作类
4 |
5 | PHP交流 QQ 群:`16455997`
6 |
7 | 环境要求:PHP >= 5.3
8 |
9 | 使用 [composer](https://getcomposer.org/)
10 |
11 | ```shell
12 | composer require "pfinal/database"
13 | ```
14 |
15 | ```php
16 | 'mysql:host=localhost;dbname=test',
22 | 'username' => 'root',
23 | 'password' => '',
24 | 'charset' => 'utf8',
25 | 'tablePrefix' => 'db_',
26 | );
27 |
28 | $db = new \PFinal\Database\Builder($config);
29 |
30 | $id = $db->table('user')->insertGetId(['username' => 'Jack', 'email' => 'jack@gmail.com']);
31 |
32 | echo $id;
33 |
34 | ```
35 |
36 | [更多用法请点击这里](doc/index.md)
37 |
38 |
39 | 如果你的项目未使用Composer,请使用下面的方式做类自动加载
40 |
41 | ```php
42 | register();
48 |
49 | ```
--------------------------------------------------------------------------------
/src/ClassLoader.php:
--------------------------------------------------------------------------------
1 | directory = $baseDirectory;
22 | $this->prefix = __NAMESPACE__ . '\\';
23 | $this->prefixLength = strlen($this->prefix);
24 | }
25 |
26 | /**
27 | * 注册自动加载器到 PHP SPL autoloader
28 | *
29 | * @param bool $prepend
30 | */
31 | public static function register($prepend = false)
32 | {
33 | spl_autoload_register(array(new self(), 'autoload'), true, $prepend);
34 | }
35 |
36 | /**
37 | * @param string $className 完整类名
38 | */
39 | public function autoload($className)
40 | {
41 | if (0 === strpos($className, $this->prefix)) {
42 | $parts = explode('\\', substr($className, $this->prefixLength));
43 | $filePath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php';
44 |
45 | if (is_file($filePath)) {
46 | require $filePath;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Relations/BelongsTo.php:
--------------------------------------------------------------------------------
1 | where([$this->ownerKey => $this->foreignValue]);
16 |
17 | return $this->findOne();
18 | }
19 |
20 | public function appendData(array $models, $name, $relations = [])
21 | {
22 | if (count($models) == 0) {
23 | return;
24 | }
25 |
26 | $relations = (array)$relations;
27 |
28 | if (isset($models[0]->$name) && count($relations) > 0) {
29 | self::appendRelationData(array_column($models, $name), $relations);
30 | return;
31 | }
32 |
33 | $ids = Util::arrayColumn($models, $this->foreignKey);
34 | $ids = array_unique($ids);
35 |
36 | $this->whereIn($this->ownerKey, $ids);
37 | $relationData = $this->findAll();
38 | if (count($relations) > 0) {
39 | $this->appendRelationData($relationData, $relations);
40 | }
41 |
42 | $relationData = Util::arrayColumn($relationData, null, $this->ownerKey);
43 |
44 | foreach ($models as $k => $v) {
45 | $models[$k][$name] = isset($relationData[$v[$this->foreignKey]]) ? $relationData[$v[$this->foreignKey]] : null;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Relations/HasOne.php:
--------------------------------------------------------------------------------
1 | where([$this->foreignKey => $this->localValue]);
16 |
17 | return $this->findOne();
18 | }
19 |
20 | public function appendData(array $models, $name, $relations = [])
21 | {
22 | if (count($models) == 0) {
23 | return;
24 | }
25 |
26 | $relations = (array)$relations;
27 |
28 | if (isset($models[0]->$name) && count($relations) > 0) {
29 | self::appendRelationData(array_column($models, $name), $relations);
30 | return;
31 | }
32 |
33 | $ids = Util::arrayColumn($models, $this->localKey);
34 | $ids = array_unique($ids);
35 |
36 | $this->whereIn($this->foreignKey, $ids);
37 |
38 | $relationData = $this->findAll();
39 | if (count($relations) > 0) {
40 | $this->appendRelationData($relationData, $relations);
41 | }
42 |
43 | $relationData = Util::arrayColumn($relationData, null, $this->foreignKey);
44 |
45 | foreach ($models as $k => $v) {
46 | $models[$k][$name] = isset($relationData[$v[$this->localKey]]) ? $relationData[$v[$this->localKey]] : null;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/tests/demo.php:
--------------------------------------------------------------------------------
1 | getMessage() . ' #' . $exception->getLine() . PHP_EOL;
19 | exit;
20 | });
21 |
22 |
23 | include __DIR__ . "/autoload.php";
24 |
25 | $config = array(
26 | 'dsn' => 'mysql:host=127.0.0.1;dbname=test',
27 | 'username' => 'root',
28 | 'password' => 'root',
29 | 'charset' => 'utf8',
30 | 'tablePrefix' => 'db_',
31 | 'reconnect' => true,
32 | );
33 |
34 | $db = new \PFinal\Database\Builder($config);
35 |
36 | $db->getConnection()->enableQueryLog();
37 |
38 | // ================= //
39 |
40 | $db->getConnection()->query('select sleep(1)');
41 | var_dump(count($db->getConnection()->getQueryLog(false)));
42 |
43 | sleep(10);
44 | //到mysql中kill此连接,或设置set global wait_timeout=5 模拟连接长时间空闲,被服务端超时关闭
45 |
46 | // show full processlist;
47 | // kill
48 |
49 | $db->getConnection()->query('select sleep(1)');
50 | var_dump(count($db->getConnection()->getQueryLog()));
51 | exit;
52 |
53 |
54 | // =================//
55 | $db->getConnection()->query('select sleep(1)');
56 | var_dump(count($db->getConnection()->getQueryLog(false)));
57 |
58 | $db->getConnection()->query('select sleep(10)');
59 |
60 | //此时到mysql中kill此连接,强制kill一个条正在执行的sql
61 | //这种情况,不应该再次重连,需要用set_error_handler,在检测到Warning时,终止运行
62 |
63 | var_dump(count($db->getConnection()->getQueryLog()));
64 |
--------------------------------------------------------------------------------
/src/Relations/HasMany.php:
--------------------------------------------------------------------------------
1 | where([$this->foreignKey => $this->localValue]);
16 |
17 | return $this->findAll();
18 | }
19 |
20 | public function appendData(array $models, $name, $relations = [])
21 | {
22 | if (count($models) == 0) {
23 | return;
24 | }
25 |
26 | $relations = (array)$relations;
27 |
28 | if (isset($models[0]->$name) && count($relations) > 0) {
29 | self::appendRelationData(array_column($models, $name), $relations);
30 | return;
31 | }
32 |
33 | $ids = Util::arrayColumn($models, $this->localKey);
34 | $ids = array_unique($ids);
35 |
36 | $this->whereIn($this->foreignKey, $ids);
37 |
38 | $relationData = $this->findAll();
39 | if (count($relations) > 0) {
40 | $this->appendRelationData($relationData, $relations);
41 | }
42 |
43 | foreach ($models as $k => $model) {
44 | $models[$k][$name] = [];
45 | }
46 |
47 | $models = Util::arrayColumn($models, null, $this->localKey);
48 |
49 | foreach ($relationData as $v) {
50 | $id = $v[$this->foreignKey];
51 |
52 | //PHP 7.1.16
53 | //ErrorException Indirect modification of overloaded element of XXX has no effect
54 | //$models[$id][$name][] = $v;
55 |
56 | $temp = $models[$id][$name];
57 | $temp[] = $v;
58 | $models[$id][$name] = $temp;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/tests/DbTest2.php:
--------------------------------------------------------------------------------
1 | 'mysql:host=127.0.0.1;dbname=test',
10 | 'username' => 'root',
11 | 'password' => 'root',
12 | 'charset' => 'utf8',
13 | 'tablePrefix' => 'db_',
14 | );
15 |
16 | return new \PFinal\Database\Connection($config);
17 | }
18 |
19 | public function testConnection()
20 | {
21 |
22 | $conn = self::getConn();
23 |
24 | //主库连接
25 | $this->assertTrue($conn->getPdo() === $conn->getPdo());
26 |
27 | $sql = 'DROP TABLE IF EXISTS db_test2';
28 | $conn->execute($sql);
29 |
30 | $sql = 'CREATE TABLE `db_test2` (
31 | `user_id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
32 | `username` varchar(255) NOT NULL,
33 | `auth_key` varchar(32) NOT NULL DEFAULT "",
34 | `password_hash` varchar(255) NOT NULL DEFAULT "",
35 | `password_reset_token` varchar(255) DEFAULT NULL,
36 | `email` varchar(255) NOT NULL DEFAULT "",
37 | `status` smallint(6) NOT NULL DEFAULT 10,
38 | `created_at` int(11) NOT NULL DEFAULT 0,
39 | `updated_at` int(11) NOT NULL DEFAULT 0
40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8';
41 |
42 | $conn->execute($sql);
43 |
44 |
45 | $sql = 'INSERT INTO {{%test2}} (username,status) VALUES (?,?)';
46 |
47 | $count = 0;
48 |
49 | $count += $conn->execute($sql, ['Summer', 10]);
50 | $count += $conn->execute($sql, ['Ethan', 20]);
51 | $count += $conn->execute($sql, ['Jack', 10]);
52 |
53 | $this->assertTrue($count == 3);
54 | $this->assertTrue($conn->getLastInsertId() == 3);
55 |
56 | $db = new \PFinal\Database\Builder();
57 | $db->setConnection(self::getConn());
58 |
59 | $db->getConnection()->enableQueryLog();
60 |
61 | $user = $db->table('test2')->wherePk(1)->findOne();
62 | $this->assertTrue($user['username'] === 'Summer');
63 |
64 |
65 | //dump($db->getConnection()->getQueryLog());
66 | }
67 |
68 |
69 | }
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/DataProvider.php:
--------------------------------------------------------------------------------
1 | query = $query;
25 | $this->pageConfig = $pageConfig;
26 | }
27 |
28 | /**
29 | * @return array
30 | */
31 | public function getData()
32 | {
33 | $this->fillData();
34 | return $this->data;
35 | }
36 |
37 | /**
38 | * @return Pagination
39 | */
40 | public function getPage()
41 | {
42 | if ($this->page === null) {
43 |
44 | if (class_exists('Leaf\\Application') && isset(\Leaf\Application::$app)) {
45 | $this->page = \Leaf\Application::$app->make('Leaf\\Pagination');
46 | } else {
47 | $this->page = new Pagination();
48 | }
49 |
50 | $this->page->config($this->pageConfig);
51 |
52 | $countQuery = clone $this->query;
53 | $this->page->itemCount = $countQuery->count('*');
54 | }
55 |
56 | return $this->page;
57 | }
58 |
59 | /**
60 | * 分页按扭
61 | *
62 | * @param string $baseUrl
63 | * @param null $prefix
64 | * @param null $suffix
65 | * @return string
66 | */
67 | public function createLinks($baseUrl = '', $prefix = null, $suffix = null)
68 | {
69 | return $this->getPage()->createLinks($baseUrl, $prefix, $suffix);
70 | }
71 |
72 | /**
73 | * @return array
74 | */
75 | public function jsonSerialize()
76 | {
77 | return array(
78 | 'page' => $this->getPage(),
79 | 'data' => $this->getData(),
80 | );
81 | }
82 |
83 | private function fillData()
84 | {
85 | if ($this->data === null) {
86 | $this->data = $this->query->limit($this->getPage()->limit)->findAll();
87 | }
88 | }
89 |
90 | public function offsetExists($offset)
91 | {
92 | $this->fillData();
93 | return isset($this->data[$offset]);
94 | }
95 |
96 | public function offsetGet($offset)
97 | {
98 | $this->fillData();
99 | return $this->data[$offset];
100 | }
101 |
102 | public function offsetSet($offset, $value)
103 | {
104 | $this->fillData();
105 | $this->data[$offset] = $value;
106 | }
107 |
108 | public function offsetUnset($offset)
109 | {
110 | $this->fillData();
111 | unset($this->data[$offset]);
112 | }
113 |
114 | private $position = 0;
115 |
116 | public function current()
117 | {
118 | $this->fillData();
119 | return $this->data[$this->position];
120 | }
121 |
122 | public function next()
123 | {
124 | ++$this->position;
125 | }
126 |
127 | public function key()
128 | {
129 | return $this->position;
130 | }
131 |
132 | public function valid()
133 | {
134 | $this->fillData();
135 | return isset($this->data[$this->position]);
136 | }
137 |
138 | public function rewind()
139 | {
140 | $this->position = 0;
141 | }
142 |
143 | public function count()
144 | {
145 | $this->fillData();
146 | return count($this->data);
147 | }
148 | }
--------------------------------------------------------------------------------
/doc/index.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 实例化对象
4 |
5 | ```php
6 | $config = array(
7 | 'dsn' => 'mysql:host=localhost;dbname=test',
8 | 'username' => 'root',
9 | 'password' => '',
10 | 'charset' => 'utf8',
11 | 'tablePrefix' => '',
12 | );
13 |
14 | $db = new \PFinal\Database\Builder($config);
15 | ```
16 |
17 | 新增数据
18 |
19 | ```php
20 | $user = ['name' => 'jack', 'email' => 'jack@gmail.com'];
21 | $bool = $db->table('user')->insert($user);
22 | $userId = $db->table('user')->insertGetId($user);
23 | ```
24 |
25 | 更新数据
26 |
27 | ```php
28 | //UPDATE `user` SET `name` = 'mary' WHERE `id` = 1
29 | $rowCount = $db->table('user')->where('id=?', 1)->update(['name' => 'mary']);
30 |
31 | //跟据主键更新,自动检测主键字段
32 | $rowCount = $db->table('user')->wherePk(1)->update(['name' => 'mary']);
33 |
34 | //字段自增(例如增加积分的场景)
35 | //UPDATE `user` SET `age` = `age` + 1, `updated_at` = '1504147628' WHERE id = 1
36 | $db->table('user')->where('id=?', 1)->increment('age', 1, ['updated_at' => time()]);
37 | ```
38 |
39 | 删除数据
40 |
41 | ```php
42 | $rowCount = $db->table('user')->where('id=?', 1)->delete();
43 | ```
44 |
45 | 查询数据
46 |
47 | ```php
48 | //查询user表所有数据
49 | $users = $db->table('user')->findAll();
50 |
51 | //跳过4条,返回2条
52 | $users = $db->table('user')->limit('4, 2')->findAll();
53 |
54 | //排序
55 | $users = $db->table('user')->where('status=?', 1)->limit('4, 2')->orderBy('id desc')->findAll();
56 |
57 | //MySQL随机排序
58 | $users = $db->table('user')->orderBy(new \PFinal\Database\Expression('rand()'))->findAll();
59 |
60 | //返回单条数据
61 | $user = $db->table('user')->where('id=?', 1)->findOne();
62 |
63 | //查询主键为1的单条数据
64 | $user = $db->table('user')->findByPk(1);
65 |
66 | //统计查询
67 | $count = $db->table('user')->count();
68 |
69 | $maxUserId = $db->table('user')->max('id');
70 |
71 | $minUserId= $db->table('user')->min('id');
72 |
73 | $avgAge = $db->table('user')->avg('age');
74 |
75 | $sumScore = $db->table('user')->sum('score');
76 |
77 | // 返回数据的方法,必须在最后面调用,且一次链式调用中只能用一个,例如:findAll findOne findByPk count max min avg sum 等方法。
78 | // where 方法支持多次调用,默认用and连接。
79 | // where、whereIn、wherePk、limit、orderBy、field、join、groupBy having 这几个方法调用先后顺序无关。
80 |
81 | ```
82 |
83 | 查询条件
84 |
85 | ```php
86 | //SELECT * FROM `user` WHERE `name`='jack' AND `status`='1'
87 | $users = $db->table('user')->where('name=? and status=?', ['jack', '1'])->findAll();
88 | $users = $db->table('user')->where('name=:name and status=:status', ['name' => 'jack', 'status' => '1'])->findAll();
89 | $users = $db->table('user')->where(['name' => 'jack', 'status' => 1])->findAll();
90 | $users = $db->table('user')->where(['name' => 'jack'])->where(['status' => 1])->findAll();
91 | //以上4种写法,是一样的效果
92 |
93 | //SELECT * FROM `user` WHERE `name` like '%j%'
94 | $users = $db->table('user')->where('name like ?', '%j%')->findAll();
95 |
96 | //SELECT * FROM `user` WHERE `id` IN (1, 2, 3)
97 | $users = $db->table('user')->whereIn('id', [1, 2, 3])->findAll();
98 |
99 | //SELECT * FROM `user` WHERE `name`='jack' OR `name`='mary'
100 | $users = $db->table('user')->where('name=? or name =? ', ['jack', 'mary'])->findAll();
101 | $users = $db->table('user')->where('name=?', 'jack')->where('name=?', 'mary', false)->findAll();
102 |
103 | ```
104 |
105 | Group By
106 |
107 | ```php
108 | $res = $db->table('tests')
109 | ->field('status')
110 | ->groupBy('status')
111 | ->having('status>:status', ['status' => 1])
112 | ->findAll();
113 | ```
114 |
115 | Join
116 |
117 | ```php
118 | $res = $db->table('user as u')
119 | ->join('info as i','u.id=i.user_id')
120 | ->field('u.*, i.address')
121 | ->orderBy('u.id')
122 | ->where('u.id>?', 10)
123 | ->findAll();
124 | ```
125 |
126 | 事务
127 |
128 | ```php
129 | $db->getConnection()->beginTransaction(); //开启事务
130 | $db->getConnection()->commit(); //提交事务
131 | $db->getConnection()->rollBack(); //回滚事务
132 | ```
133 |
134 | 数据分页
135 |
136 | ```php
137 | $dataProvider = $db->table('tests')->paginate();
138 | var_dump($dataProvider->getData());
139 | echo $dataProvider->createLinks();
140 | ```
141 |
142 | 处理大量的数据
143 |
144 | ```php
145 | //cursor
146 | foreach (DB::table('user')->where('status=1')->cursor() as $user) {
147 | // ...
148 | }
149 |
150 | //chunk
151 | DB::select('user')->where('status=1')->orderBy('id')->chunk(100, function ($users) {
152 | foreach ($users as $user) {
153 | // ...
154 | }
155 | });
156 |
157 | //chunkById
158 | DB::select('user')->where('status=1')->chunkById(100, function ($users) {
159 | foreach ($users as $user) {
160 | // ...
161 | }
162 | });
163 |
164 | ```
165 |
166 | 调试SQL
167 |
168 | ```php
169 | $sql = $db->table('user')->where(['name'=>'Jack'])->toSql();
170 | var_dump($sql);
171 |
172 | $db->getConnection()->enableQueryLog();
173 | $user = $db->table('user')->findOne();
174 | $sqls = $db->getConnection()->getQueryLog();
175 | var_dump($sqls);
176 |
177 | ```
--------------------------------------------------------------------------------
/tests/DbTest.php:
--------------------------------------------------------------------------------
1 | 'mysql:host=127.0.0.1;dbname=test',
10 | 'username' => 'root',
11 | 'password' => 'root',
12 | 'charset' => 'utf8',
13 | 'tablePrefix' => 'db_',
14 | 'slave' => array(
15 | array(
16 | 'dsn' => 'mysql:host=127.0.0.1;dbname=test',
17 | 'username' => 'root',
18 | 'password' => 'root',
19 | )
20 | ),
21 | );
22 |
23 | return new \PFinal\Database\Connection($config);
24 | }
25 |
26 | public function testConnection()
27 | {
28 |
29 | $conn = self::getConn();
30 |
31 | //主库连接
32 | $this->assertTrue($conn->getPdo() === $conn->getPdo());
33 | //从库连接
34 | $this->assertTrue($conn->getPdo() !== $conn->getReadPdo());
35 |
36 | $sql = 'DROP TABLE IF EXISTS db_test';
37 | $conn->execute($sql);
38 |
39 | $sql = 'CREATE TABLE `db_test` (
40 | `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
41 | `username` varchar(255) NOT NULL,
42 | `auth_key` varchar(32) NOT NULL DEFAULT "",
43 | `password_hash` varchar(255) NOT NULL DEFAULT "",
44 | `password_reset_token` varchar(255) DEFAULT NULL,
45 | `email` varchar(255) NOT NULL DEFAULT "",
46 | `status` smallint(6) NOT NULL DEFAULT 10,
47 | `created_at` int(11) NOT NULL DEFAULT 0,
48 | `updated_at` int(11) NOT NULL DEFAULT 0
49 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8';
50 |
51 | $conn->execute($sql);
52 |
53 |
54 | $sql = 'INSERT INTO {{%test}} (username,status) VALUES (?,?)';
55 |
56 | $count = 0;
57 |
58 | $count += $conn->execute($sql, ['Summer', 10]);
59 | $count += $conn->execute($sql, ['Ethan', 20]);
60 | $count += $conn->execute($sql, ['Jack', 10]);
61 |
62 | $this->assertTrue($count == 3);
63 | $this->assertTrue($conn->getLastInsertId() == 3);
64 |
65 | }
66 |
67 |
68 | public function testDb()
69 | {
70 | $db = new \PFinal\Database\Builder();
71 | $db->setConnection(self::getConn());
72 |
73 | $db->getConnection()->enableQueryLog();
74 |
75 | $rowCount = $db->table('{{%test}}')->insert(['username' => 'aaa', 'status' => 10]);
76 | $this->assertTrue($rowCount == 1);
77 |
78 | $id = $db->table('{{db_test}}')->insertGetId(['username' => 'bbb', 'status' => 10]);
79 | $this->assertTrue($id >0);
80 |
81 | $arr = $db->findAllBySql('SELECT * FROM {{%test}} WHERE status=? LIMIT 2', [10]);
82 | $this->assertTrue(count($arr) == 2);
83 |
84 | $arr = $db->table('test')->where('id=1 or id=2')->findAll();
85 | $this->assertTrue(count($arr) == 2);
86 | $this->assertTrue($arr[0]['id'] == 1 && $arr[1]['id'] == 2);
87 |
88 | $arr = $db->table('test')->where('id=?', [1])->where('id=:id', ['id' => 2], false)->findAll();
89 | $this->assertTrue(count($arr) == 2);
90 | $this->assertTrue($arr[0]['id'] == 1 && $arr[1]['id'] == 2);
91 |
92 | $row = $db->table('test')->findOne('id=?', [2]);
93 | $this->assertTrue($row['username'] === 'Ethan');
94 |
95 | $row = $db->table('test')->findOne(['id' => 2]);
96 | $this->assertTrue($row['username'] === 'Ethan');
97 |
98 |
99 | $rowCount = $db->table('test')->update(['username' => 'new'], 'id=?', [2]);
100 | $this->assertTrue($rowCount == 1);
101 | $row = $db->table('test')->findByPk(2);
102 | $this->assertTrue($row['username'] === 'new');
103 |
104 | $rowCount = $db->table('test')->update(['username' => 'new2'], ['id' => 2]);
105 | $this->assertTrue($rowCount == 1);
106 | $row = $db->table('test')->findByPk(2);
107 | $this->assertTrue($row['username'] === 'new2');
108 |
109 | $rowCount = $db->table('test')->where('id=?', [2])->update(['username' => 'new3']);
110 | $this->assertTrue($rowCount == 1);
111 | $row = $db->table('test')->where(['id' => 2])->findOne();
112 | $this->assertTrue($row['username'] === 'new3');
113 |
114 | $rowCount = $db->table('test')->where('id=?', [2])->increment('status');
115 | $this->assertTrue($rowCount == 1);
116 | $row = $db->table('test')->where(['id' => 2])->findOne();
117 | $this->assertTrue($row['status'] == 21);
118 |
119 |
120 | $rowCount = $db->table('test')->where('id=?', [3])->delete();
121 | $this->assertTrue($rowCount == 1);
122 |
123 |
124 | //count()、sum()、max()、min()、avg()
125 | $count = $db->table('test')->where('id>?', [3])->count();
126 | $this->assertTrue($count == 2);
127 |
128 | $arr = $db->table('test')->loadDefaultValues();
129 | $this->assertTrue($arr['status'] == 10);
130 |
131 | $arr = $db->table('test')->loadDefaultValues(new stdClass());
132 | $this->assertTrue($arr->status == 10);
133 |
134 |
135 | //dump($db->getConnection()->getQueryLog());
136 | }
137 |
138 | }
139 |
140 |
141 |
--------------------------------------------------------------------------------
/src/ModelTrait.php:
--------------------------------------------------------------------------------
1 | table(static::tableName(), self::convertTableName(get_called_class()))->asEntity(get_called_class()), $name],
57 | $arguments);
58 | }
59 |
60 | public function loadDefaultValues()
61 | {
62 | return DB::getInstance()->table(static::tableName())->loadDefaultValues($this);
63 | }
64 |
65 | private static function convertTableName($className)
66 | {
67 | //去掉namespace
68 | $name = rtrim(str_replace('\\', '/', $className), '/\\');
69 | if (($pos = mb_strrpos($name, '/')) !== false) {
70 | $name = mb_substr($name, $pos + 1);
71 | }
72 |
73 | //大写转为下划线风格
74 | $name = trim(strtolower(preg_replace('/[A-Z]/', '_\0', $name)), '_');
75 |
76 | return $name;
77 | }
78 |
79 | /**
80 | * 一对一
81 | *
82 | * @param string $related
83 | * @param string $foreignKey
84 | * @param string $localKey
85 | * @return $this
86 | */
87 | public function hasOne($related, $foreignKey = null, $localKey = 'id')
88 | {
89 | if ($foreignKey === null) {
90 | $foreignKey = self::convertTableName(get_called_class()) . '_id';
91 | }
92 |
93 | $hasOne = new HasOne();
94 | $hasOne->setConnection(DB::getInstance()->getConnection());
95 |
96 | $obj = $hasOne->table($related::tableName())->asEntity($related);
97 |
98 | $obj->foreignKey = $foreignKey;
99 | $obj->localKey = $localKey;
100 | $obj->localValue = $this->{$localKey};
101 |
102 | return $obj;
103 | }
104 |
105 | /**
106 | * 从属关联
107 | *
108 | * @param string $related
109 | * @param null $foreignKey
110 | * @param string $localKey
111 | * @return $this
112 | */
113 | public function belongsTo($related, $foreignKey = null, $ownerKey = 'id')
114 | {
115 | if ($foreignKey === null) {
116 | $foreignKey = self::convertTableName($related) . '_id';
117 | }
118 |
119 | $hasOne = new BelongsTo();
120 | $hasOne->setConnection(DB::getInstance()->getConnection());
121 |
122 | $obj = $hasOne->table($related::tableName())->asEntity($related);
123 |
124 | $obj->ownerKey = $ownerKey;
125 | $obj->foreignKey = $foreignKey;
126 | $obj->foreignValue = $this->{$foreignKey};
127 |
128 | return $obj;
129 | }
130 |
131 | /**
132 | * hasMany
133 | *
134 | * @param string $related
135 | * @param null $foreignKey
136 | * @param string $localKey
137 | * @return $this
138 | */
139 | public function hasMany($related, $foreignKey = null, $localKey = 'id')
140 | {
141 | if ($foreignKey === null) {
142 | $foreignKey = self::convertTableName(get_called_class()) . '_id';
143 | }
144 |
145 | $hasOne = new HasMany();
146 | $hasOne->setConnection(DB::getInstance()->getConnection());
147 |
148 | $obj = $hasOne->table($related::tableName())->asEntity($related);
149 |
150 | $obj->foreignKey = $foreignKey;
151 | $obj->localKey = $localKey;
152 | $obj->localValue = $this->{$localKey};
153 |
154 | return $obj;
155 | }
156 |
157 | /**
158 | * 渴求式加载
159 | *
160 | * eg:
161 | * Blog::with('category')->findAll()
162 | * Favorite::with('project.city', 'project.user')->findAll()
163 | *
164 | * @param string|array $relations 关联名称
165 | */
166 | public static function with($relations)
167 | {
168 | $relations = is_string($relations) ? func_get_args() : $relations;
169 |
170 | return DB::getInstance()->table(static::tableName(), self::convertTableName(get_called_class()))->asEntity(get_called_class())->afterFind(function ($models) use ($relations) {
171 | RelationBase::appendRelationData($models, $relations);
172 | });
173 | }
174 |
175 |
176 | public function __get($getter)
177 | {
178 | if (!method_exists($this, $getter)) {
179 | return parent::__get($getter);
180 | }
181 |
182 | $relation = $this->$getter();
183 |
184 | return call_user_func($relation);
185 | }
186 |
187 | }
--------------------------------------------------------------------------------
/src/Pagination.php:
--------------------------------------------------------------------------------
1 | createLinks('article/index')
49 | * article/index
50 | * article/index ?page= 2 &username=jack&status=10
51 | *
52 | * $page->createLinks('article/index', '/', '.html')
53 | * article/index .html ?username=jack&status=10
54 | * article/index / 2 .html ?username=jack&status=10
55 | *
56 | * @param string $baseUrl
57 | * @param null $prefix 页码数字与$baseUrl之间的内容 默认为`?page=`
58 | * @param null $suffix 页码数字之后的内容,默认为空 示例: `.html`
59 | * @return string
60 | * 首页url $baseUrl + $suffix + query_string($_SERVER['QUERY_STRING'])
61 | * 其它页面 $baseUrl + $prefix + $suffix + query_string
62 | */
63 | public function createLinks($baseUrl, $prefix = null, $suffix = null)
64 | {
65 | $this->url = $baseUrl;
66 | $this->prefix = $prefix;
67 | $this->suffix = $suffix;
68 |
69 | if (class_exists('Twig_Markup')) { // twig template
70 | return new \Twig_Markup($this->createPageLinks(), 'utf-8');
71 | }
72 |
73 | return $this->createPageLinks();
74 | }
75 |
76 | /**
77 | * 初始化
78 | */
79 | protected function init()
80 | {
81 | //计算总页数
82 | $this->pageCount = ceil($this->itemCount / $this->pageSize);
83 |
84 | //当前页码
85 | if (empty($this->currentPage) && isset($_REQUEST[$this->pageVar])) {
86 | $this->currentPage = intval($_REQUEST[$this->pageVar]);
87 | }
88 | $this->currentPage = intval($this->currentPage);
89 |
90 | //最小页码判断
91 | if ($this->currentPage < 1) {
92 | $this->currentPage = 1;
93 | }
94 |
95 | //偏移量 (当前页-1)*每页条数
96 | $this->offset = ($this->currentPage - 1) * $this->pageSize;
97 |
98 | if ($this->currentPage > $this->pageCount) {
99 | $this->currentPage = $this->pageCount;
100 | }
101 |
102 | //上一页
103 | $this->prevPage = ($this->currentPage <= 1) ? 1 : $this->currentPage - 1;
104 |
105 | //下一页
106 | $this->nextPage = ($this->currentPage == $this->pageCount) ? $this->pageCount : $this->currentPage + 1;
107 | }
108 |
109 | /**
110 | * 生成页码超链接
111 | * @return string
112 | */
113 | protected function createPageLinks()
114 | {
115 | $this->init();
116 |
117 | if ($this->pageCount <= 1) {
118 | return '';
119 | }
120 |
121 | $buttonCount = $this->maxButtonCount * 2 + 1;
122 |
123 | //开始数字
124 | if ($this->currentPage <= $this->maxButtonCount || $this->pageCount <= $buttonCount) {
125 | $ctrl_begin = 1;
126 | } else if ($this->currentPage > $this->pageCount - ($this->maxButtonCount)) {
127 | $ctrl_begin = $this->pageCount - ($this->maxButtonCount * 2);
128 | } else {
129 | $ctrl_begin = $this->currentPage - $this->maxButtonCount;
130 | }
131 |
132 | //结束数字
133 | $ctrl_end = $ctrl_begin + $buttonCount - 1;
134 |
135 | //不能大于总页数
136 | if ($ctrl_end > $this->pageCount) {
137 | $ctrl_end = $this->pageCount;
138 | }
139 |
140 | $ctrl_num_html = "";
141 | for ($i = $ctrl_begin; $i <= $ctrl_end; $i++) {
142 | if ($i == $this->currentPage) {
143 | //当前页,不加超链接
144 | $ctrl_num_html .= "<{$this->disabledTag} class='{$this->selectedPageCssClass}' >{$i}{$this->disabledTag}>";
145 | } else {
146 | $url = $this->createPageLink($i);
147 | $ctrl_num_html .= "{$i}";
148 | }
149 | }
150 |
151 | //判断是否需要加上省略号
152 | if ($ctrl_begin != 1) {
153 | $url = $this->createPageLink(1);
154 | $ctrl_num_html = "1<{$this->disabledTag}>...{$this->disabledTag}>" . $ctrl_num_html;
155 | }
156 | if ($ctrl_end != $this->pageCount) {
157 | $url = $this->createPageLink($this->pageCount);
158 | $ctrl_num_html .= "<{$this->disabledTag}>...{$this->disabledTag}>{$this->pageCount}";
159 | }
160 |
161 | //上一页
162 | if ($this->currentPage == 1) {
163 | $prev = "<{$this->disabledTag} class='{$this->prevPageCssClass} {$this->disabledCssClass}'>{$this->prevPageLabel}{$this->disabledTag}>";
164 | } else {
165 | $url = $this->createPageLink($this->prevPage);
166 | $prev = "{$this->prevPageLabel}";
167 | }
168 |
169 | //下一页
170 | if ($this->currentPage == $this->pageCount) {
171 | $next = "<{$this->disabledTag} class='{$this->nextPageCssClass} {$this->disabledCssClass}'>{$this->nextPageLabel}{$this->disabledTag}>";
172 | } else {
173 | $url = $this->createPageLink($this->nextPage);
174 | $next = "{$this->nextPageLabel}";
175 | }
176 |
177 | //控制翻页链接
178 | $html = "className}\">";
179 | $html .= $prev . ' ';
180 | $html .= $ctrl_num_html;
181 | $html .= ' ' . $next;
182 | $html .= "";
183 | return $html;
184 | }
185 |
186 | protected function createPageLink($num)
187 | {
188 | if ($num == 1) {
189 | return $this->appendSuffix($this->url);
190 | }
191 |
192 | $prefix = $this->prefix;
193 | if ($prefix === null) {
194 | $s = strpos($this->url, '?') === false ? '?' : '&';
195 | $prefix = $s . urlencode($this->pageVar) . '=';
196 | }
197 |
198 | return $this->appendSuffix($this->url . $prefix . $num);
199 | }
200 |
201 | /**
202 | * @return string
203 | */
204 | protected function appendSuffix($url)
205 | {
206 | $url .= $this->suffix;
207 |
208 | //拼接query参数
209 | $get = isset($_GET) ? $_GET : array();
210 | if ($this->prefix === null) {
211 | unset($get[$this->pageVar]);//去除page参数
212 | }
213 | if (count($get) > 0) {
214 | $s = strpos($url, '?') === false ? '?' : '&';
215 | $url .= $s . http_build_query($get);
216 | }
217 | return $url;
218 | }
219 |
220 |
221 | /**
222 | * 实现JsonSerializable接口,方便转为json时自定义数据。
223 | * @return array
224 | */
225 | public function jsonSerialize()
226 | {
227 | if (static::$jsonSerializer) {
228 | $cb = static::$jsonSerializer;
229 | return $cb($this);
230 | }
231 |
232 | return array(
233 | 'itemCount' => intval($this->itemCount),//总记录数
234 | 'currentPage' => intval($this->currentPage),//当前页码
235 | 'offset' => intval($this->offset), //数据库查询的偏移量(查询开始的记录)
236 | 'pageSize' => intval($this->pageSize),//每页显示记录数
237 | 'pageCount' => intval($this->pageCount),//总页数
238 | 'prevPage' => intval($this->prevPage),//当前的上一页码
239 | 'nextPage' => intval($this->nextPage),//当前的下一页码
240 | );
241 | }
242 |
243 | public function __get($name)
244 | {
245 | $this->init();
246 | switch ($name) {
247 | case 'limit':
248 | return intval($this->offset) . ', ' . intval($this->pageSize);
249 | default:
250 | return $this->$name;
251 | }
252 | }
253 |
254 | public function __set($name, $value)
255 | {
256 | $this->$name = $value;
257 | }
258 |
259 | public function __isset($name)
260 | {
261 | return isset($this->$name);
262 | }
263 |
264 | public function config($config)
265 | {
266 | if (is_array($config)) {
267 | foreach ($config as $k => $v) {
268 | $this->$k = $v;
269 | }
270 | }
271 | }
272 | }
273 |
274 | /*
275 | .pagination {
276 | padding:3px; margin:3px; text-align:center;
277 | }
278 | .pagination a,.pagination span {
279 | border:#dddddd 1px solid;
280 | text-decoration:none;
281 | color:#666666; padding: 5px 10px; margin-right:4px;
282 | }
283 | .pagination a:hover {
284 | border: #a0a0a0 1px solid;
285 | }
286 | .pagination .active {
287 | font-weight:bold; background-color:#f0f0f0;
288 | }
289 | .pagination .disabled {
290 | border:#f3f3f3 1px solid;
291 | color:#aaaaaa;
292 | }
293 | */
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | 'mysql:host=localhost;dbname=test',
24 | 'host' => 'localhost',
25 | 'database' => 'test',
26 | 'username' => 'root',
27 | 'password' => '',
28 | 'charset' => 'utf8mb4',
29 | 'tablePrefix' => '',
30 | 'port' => 3306,
31 | 'options' => array(
32 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
33 | PDO::ATTR_STRINGIFY_FETCHES => false, //禁止提取的时候将数值转换为字符串
34 | PDO::ATTR_EMULATE_PREPARES => false, //禁止模拟预处理语句
35 | PDO::ATTR_CASE => PDO::CASE_NATURAL, //列名原样返回,不做大小写转换
36 | PDO::ATTR_TIMEOUT => 1, //连接超时秒数
37 | ),
38 | 'slave' => array(/*
39 | array(
40 | 'host' => '192.168.0.2',
41 | 'username' => 'root',
42 | 'password' => '',
43 | ),
44 | array(
45 | 'host' => '192.168.0.2:3307',
46 | 'username' => 'root',
47 | 'password' => '',
48 | ),*/
49 | ),
50 |
51 | //服务器超时并关闭连接时,是否自动重连 (常驻内存时按需开启)
52 | //MySQL server has gone away
53 | 'reconnect' => false,
54 | );
55 |
56 | //连接类型
57 | const CONN_TYPE_WRITE = 1; //写库
58 | const CONN_TYPE_READ = 2; //读库
59 |
60 | /**
61 | * Connection constructor.
62 | * @param array $config 配置信息
63 | */
64 | public function __construct(array $config)
65 | {
66 | $this->config = array_replace_recursive($this->config, $config);
67 | }
68 |
69 | /**
70 | * 返回用于操作主库的PDO对象 (执行"增删改"SQL语句)
71 | * @return PDO
72 | */
73 | public function getPdo()
74 | {
75 | if ($this->pdo instanceof PDO) {
76 | return $this->pdo;
77 | }
78 |
79 | $this->pdo = $this->makePdo($this->config);
80 | return $this->pdo;
81 | }
82 |
83 | public function setPdo(PDO $pdo)
84 | {
85 | $this->pdo = $pdo;
86 | }
87 |
88 | public function setReadPdo(PDO $pdo)
89 | {
90 | $this->readPdo = $pdo;
91 | }
92 |
93 | /**
94 | * 返回用于查询的PDO对象 (如果在事务中,将自动调用getPdo()以确保整个事务均使用主库)
95 | * @return PDO
96 | */
97 | public function getReadPdo()
98 | {
99 | if ($this->transactions >= 1) {
100 | return $this->getPdo();
101 | }
102 |
103 | if ($this->readPdo instanceof PDO) {
104 | return $this->readPdo;
105 | }
106 |
107 | if (!is_array($this->config['slave']) || count($this->config['slave']) == 0) {
108 | return $this->getPdo();
109 | }
110 |
111 | $slaveDbConfig = $this->config['slave'];
112 |
113 | shuffle($slaveDbConfig);
114 |
115 | do {
116 | // 取出一个打乱后的从库信息
117 | $config = array_shift($slaveDbConfig);
118 |
119 | // 使用主库信息补全从库配置
120 | $config = array_replace_recursive($this->config, $config);
121 |
122 | try {
123 | $this->readPdo = $this->makePdo($config);
124 | return $this->readPdo;
125 | } catch (PDOException $ex) {
126 | // nothing to do
127 | }
128 |
129 | } while (count($slaveDbConfig) > 0);
130 |
131 | // 从库不可用时使用主库
132 | // return $this->getPdo();
133 |
134 | throw $ex;
135 | }
136 |
137 | protected function makePdo(array $config)
138 | {
139 | if (isset($config['dsn'])) {
140 | $dsn = $config['dsn'];
141 | } else {
142 | $dsn = 'mysql:host=' . $config['host'] . ';port=' . $config['port'] . ';dbname=' . $config['database'];
143 | }
144 |
145 | $pdo = new PDO($dsn, $config['username'], $config['password'], $config['options']);
146 |
147 | if (strpos($dsn, 'mysql:') === 0) {
148 | $pdo->exec('SET NAMES ' . $pdo->quote($config['charset']));
149 | }
150 |
151 | return $pdo;
152 | }
153 |
154 | /**
155 | * 返回表前缀
156 | * @return string
157 | */
158 | protected function getTablePrefix()
159 | {
160 | return $this->config['tablePrefix'];
161 | }
162 |
163 | /**
164 | * 解析SQL中的表名
165 | * 当表前缀为"cms_"时将sql中的"{{%user}}"解析为 "`cms_user`"
166 | * 解析"[[列名]]" 为 "`列名`"
167 | * @param $sql
168 | * @return string
169 | */
170 | public function quoteSql($sql)
171 | {
172 | return preg_replace_callback(
173 | '/(\\{\\{(%?[\w\-\.\$ ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
174 | function ($matches) {
175 | if (isset($matches[3])) {
176 | return $this->quoteColumnName($matches[3]);
177 | } else {
178 | return str_replace('%', $this->getTablePrefix(), $this->quoteTableName($matches[2]));
179 | }
180 | },
181 | $sql
182 | );
183 | }
184 |
185 | /**
186 | * 执行SQL语句 (增、删、改 类型的SQL),返回受影响行数, 执行失败抛出异常
187 | * @param string $sql 执行的SQL,可以包含问号或冒号占位符,支持{{%table_name}}格式自动替换为表前缀
188 | * @param array $params 参数,对应SQL中的冒号或问号占位符
189 | * @return int 返回受景响行数
190 | */
191 | public function execute($sql, $params = array())
192 | {
193 | $sql = $this->quoteSql($sql);
194 |
195 | $statement = $this->makeStatementAndExecute(self::CONN_TYPE_WRITE, $sql, $params);
196 |
197 | return $statement->rowCount();
198 | }
199 |
200 | /**
201 | * 执行SQL语句,返回array (查询类型的SQL)
202 | * @param string $sql
203 | * @param array $params
204 | * @param array $fetchMode 为PDOStatement::setFetchMode的参数,例如为 [PDO::FETCH_ASSOC] 或 [PDO::FETCH_CLASS, 'User']
205 | * @param bool $useReadPdo 是否使用从库查询
206 | * @return array
207 | */
208 | public function query($sql, $params = array(), $fetchMode = array(PDO::FETCH_ASSOC), $useReadPdo = true)
209 | {
210 | $sql = $this->quoteSql($sql);
211 |
212 | $statement = $this->makeStatementAndExecute($useReadPdo ? self::CONN_TYPE_READ : self::CONN_TYPE_WRITE, $sql, $params);
213 |
214 | //PDOStatement::setFetchMode(int $mode)
215 | //PDOStatement::setFetchMode(int $PDO::FETCH_COLUMN, int $colno)
216 | //PDOStatement::setFetchMode(int $PDO::FETCH_CLASS, string $classname, array $ctorargs)
217 | //PDOStatement::setFetchMode(int $PDO::FETCH_INTO, object $object)
218 | call_user_func_array(array($statement, 'setFetchMode'), (array)$fetchMode);
219 |
220 | return $statement->fetchAll();
221 | }
222 |
223 | /**
224 | * 执行查询统计类型语句, 返回具体单个值, 常用于COUNT、AVG、MAX、MIN、SUM
225 | * @param $sql
226 | * @param array $params
227 | * @param bool $useReadPdo 是否使用从库查询
228 | * @return mixed 返回查询结果的第一行第一列数据, AVG、MAX、MIN、SUM查询时,如果表中一条数据都没有时,将返回null
229 | */
230 | public function queryScalar($sql, $params = array(), $useReadPdo = true)
231 | {
232 | $sql = $this->quoteSql($sql);
233 |
234 | $statement = $this->makeStatementAndExecute($useReadPdo ? self::CONN_TYPE_READ : self::CONN_TYPE_WRITE, $sql, $params);
235 |
236 | return $statement->fetchColumn(0);
237 | }
238 |
239 | /**
240 | * @param int $connType 连接类型 self::CONN_TYPE_XXX
241 | * @param string $sql
242 | * @param array $params
243 | * @return \PDOStatement
244 | */
245 | private function makeStatementAndExecute($connType, $sql, $params = array())
246 | {
247 | $i = 0;
248 | do {
249 | $i++;
250 | try {
251 |
252 | if ($connType == self::CONN_TYPE_READ) {
253 | $pdo = $this->getReadPdo();
254 | } else {
255 | $pdo = $this->getPdo();
256 | }
257 |
258 | //已连接上的服务器如果超时关闭连接后或者连接被手动kill,此方法并不会抛出PDOException,而是触发一条警告
259 | //Warning: PDO::prepare(): MySQL server has gone away
260 | //set global wait_timeout=5 连上mysql执行一条sql后,php中sleep(10),再执行下一条,就会报这个警告
261 | $statement = @$pdo->prepare($sql);
262 |
263 | $start = microtime(true);
264 |
265 | //如果执行时间较长的sql,被手动kill,则会在execute时先触发一条警告
266 | //Warning PDOStatement::execute(): MySQL server has gone away
267 | $statement->execute($params);
268 | $this->logQuery($sql, $params, $this->getElapsedTime($start));
269 | return $statement;
270 |
271 | } catch (PDOException $ex) {
272 |
273 | if ($i > 1) {
274 | throw $ex;
275 | }
276 |
277 | //如果发生特定错误,则断开连接后重试1次,否则将抛出异常对象
278 | $this->resolveDisconnect($connType, $ex);
279 | }
280 | } while (true);
281 | }
282 |
283 | // /**
284 | // * php >= 5.5
285 | // * 执行 select 语句并返回 Generator
286 | // *
287 | // * @param $sql
288 | // * @param array $params
289 | // * @param array $fetchMode
290 | // * @param bool $useReadPdo
291 | // * @return \Generator
292 | // */
293 | // public function cursor($sql, $params = array(), $fetchMode = array(PDO::FETCH_ASSOC), $useReadPdo = true)
294 | // {
295 | // $sql = $this->quoteSql($sql);
296 | //
297 | // if ($useReadPdo) {
298 | // $pdo = $this->getReadPdo();
299 | // $statement = @$pdo->prepare($sql);
300 | // } else {
301 | // $pdo = $this->getPdo();
302 | // $statement = @$pdo->prepare($sql);
303 | // }
304 | //
305 | // $start = microtime(true);
306 | // $statement->execute($params);
307 | // $this->logQuery($sql, $params, $this->getElapsedTime($start));
308 | //
309 | // //PDOStatement::setFetchMode(int $mode)
310 | // //PDOStatement::setFetchMode(int $PDO::FETCH_COLUMN, int $colno)
311 | // //PDOStatement::setFetchMode(int $PDO::FETCH_CLASS, string $classname, array $ctorargs)
312 | // //PDOStatement::setFetchMode(int $PDO::FETCH_INTO, object $object)
313 | // call_user_func_array(array($statement, 'setFetchMode'), (array)$fetchMode);
314 | //
315 | // while ($row = $statement->fetch()) {
316 | // yield $row;
317 | // }
318 | // }
319 |
320 | /**
321 | * 返回最后插入行的ID或序列值
322 | * PDO::lastInsertId
323 | * @param null $sequence 序列名称
324 | * @return int
325 | */
326 | public function getLastInsertId($sequence = null)
327 | {
328 | return $this->getPdo()->lastInsertId($sequence);
329 | }
330 |
331 | /**
332 | * 开启事务
333 | */
334 | public function beginTransaction()
335 | {
336 | ++$this->transactions;
337 | if ($this->transactions == 1) {
338 |
339 | try {
340 | $this->getPdo()->beginTransaction();
341 | } catch (PDOException $ex) {
342 |
343 | //断开连接后重试
344 | $this->resolveDisconnect(self::CONN_TYPE_WRITE, $ex);
345 |
346 | $this->getPdo()->beginTransaction();
347 | }
348 | }
349 | }
350 |
351 | /**
352 | * 提交事务
353 | */
354 | public function commit()
355 | {
356 | if ($this->transactions == 1) {
357 | $this->getPdo()->commit();
358 | }
359 | --$this->transactions;
360 | }
361 |
362 | /**
363 | * 回滚事务
364 | */
365 | public function rollBack()
366 | {
367 | if ($this->transactions == 1) {
368 | $this->transactions = 0;
369 | $this->getPdo()->rollBack();
370 | } else {
371 | --$this->transactions;
372 | }
373 | }
374 |
375 | public function getTransactions()
376 | {
377 | return $this->transactions;
378 | }
379 |
380 | /**
381 | * 断开数据库链接
382 | * @param int $connType self::CONN_TYPE_XXX
383 | */
384 | public function disconnect($connType = null)
385 | {
386 | if ($connType == self::CONN_TYPE_WRITE) {
387 | $this->pdo = null;
388 | return;
389 | }
390 |
391 | if ($connType == self::CONN_TYPE_READ) {
392 |
393 | //没有配置读库,只使用了主库
394 | if (!is_array($this->config['slave']) || count($this->config['slave']) == 0) {
395 | $this->pdo = null;
396 | }
397 |
398 | $this->readPdo = null;
399 | return;
400 | }
401 |
402 | $this->pdo = null;
403 | $this->readPdo = null;
404 | }
405 |
406 | /**
407 | * 特定情况下断开连接
408 | * @param int $connType CONN_TYPE_XXX
409 | * @param PDOException $ex
410 | */
411 | private function resolveDisconnect($connType, PDOException $ex)
412 | {
413 | // https://dev.mysql.com/doc/refman/5.7/en/gone-away.html
414 | if ($this->config['reconnect']
415 | && $this->getTransactions() == 0
416 | && in_array($ex->errorInfo[1], array(2006, 2013))) {
417 |
418 | $this->disconnect($connType);
419 | return;
420 | }
421 |
422 | throw $ex;
423 | }
424 |
425 | /**
426 | * 解析SQL中的占位置("?"或":") 用于调试SQL
427 | * @param string $sql
428 | * @param array $params
429 | * @return string
430 | */
431 | public function parsePlaceholder($sql, array $params = array())
432 | {
433 | // 一次替换一个问号
434 | $count = substr_count($sql, '?');
435 | for ($i = 0; $i < $count; $i++) {
436 | $sql = preg_replace('/\?/', $this->getPdo()->quote($params[$i]), $sql, 1);
437 | }
438 |
439 | // 替换冒号
440 | $sql = preg_replace_callback('/:(\w+)/', function ($matches) use ($params) {
441 | if (isset($params[$matches[1]])) {
442 | return $this->getPdo()->quote($params[$matches[1]]);
443 | } else if (isset($params[':' . $matches[1]])) {
444 | return $this->getPdo()->quote($params[':' . $matches[1]]);
445 | }
446 | return $matches[0];
447 | }, $sql);
448 |
449 | return $sql;
450 | }
451 |
452 | /**
453 | * 给表名加引号
454 | * 如果有前缀,前缀也将被加上引号
455 | * 如果已加引号,或包含 '(' or '{{', 将不做处理
456 | * @param string $name
457 | * @return string
458 | */
459 | protected function quoteTableName($name)
460 | {
461 | if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
462 | return $name;
463 | }
464 | if (strpos($name, '.') === false) {
465 | return $this->quoteSimpleTableName($name);
466 | }
467 | $parts = explode('.', $name);
468 | foreach ($parts as $i => $part) {
469 | $parts[$i] = $this->quoteSimpleTableName($part);
470 | }
471 |
472 | return implode('.', $parts);
473 | }
474 |
475 | /**
476 | * 给列名加引号
477 | * 如果有前缀,前缀也将被加上引号
478 | * 如果列名已加引号,或包含 '(', '[[' or '{{', 将不做处理
479 | * @param string $name
480 | * @return string
481 | */
482 | protected function quoteColumnName($name)
483 | {
484 | if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
485 | return $name;
486 | }
487 | if (($pos = strrpos($name, '.')) !== false) {
488 | $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
489 | $name = substr($name, $pos + 1);
490 | } else {
491 | $prefix = '';
492 | }
493 |
494 | return $prefix . $this->quoteSimpleColumnName($name);
495 | }
496 |
497 | /**
498 | * 给表名加上引号
499 | * 表名为无前缀的简单列名
500 | * @param string $name
501 | * @return string
502 | */
503 | protected function quoteSimpleTableName($name)
504 | {
505 | return strpos($name, '`') !== false ? $name : '`' . $name . '`';
506 | }
507 |
508 | /**
509 | * 给列名加上引号
510 | * 列名为无前缀的简单列名
511 | * @param string $name
512 | * @return string
513 | */
514 | protected function quoteSimpleColumnName($name)
515 | {
516 | return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`';
517 | }
518 |
519 | /**
520 | * 开启记录所有SQL, 如果不开启, 默认只记录最后一次执行的SQL
521 | */
522 | public function enableQueryLog()
523 | {
524 | $this->enableQueryLog = true;
525 | }
526 |
527 | /**
528 | * 禁止记录所有SQL
529 | */
530 | public function disableQueryLog()
531 | {
532 | $this->enableQueryLog = false;
533 | }
534 |
535 | /**
536 | * 记录SQL
537 | * @param $sql
538 | * @param array $params
539 | */
540 | protected function logQuery($sql, $params = array(), $time = null)
541 | {
542 | if ($this->enableQueryLog) {
543 | $this->queryLog[] = compact('sql', 'params', 'time');
544 | } else {
545 | $this->queryLog = array(compact('sql', 'params', 'time'));
546 | }
547 | }
548 |
549 | /**
550 | * 返回执行的SQL
551 | * @param bool $clear 是否清空
552 | * @return array
553 | */
554 | public function getQueryLog($clear = true)
555 | {
556 | $data = $this->queryLog;
557 |
558 | if ($clear) {
559 | $this->queryLog = array();
560 | }
561 |
562 | return $data;
563 | }
564 |
565 |
566 | /**
567 | * 返回最近一次执行的sql语句
568 | * @return string
569 | */
570 | public function getLastSql()
571 | {
572 | if (count($this->queryLog) == 0) {
573 | return null;
574 | }
575 | $queryLog = end($this->queryLog);
576 | return $this->parsePlaceholder($queryLog['sql'], $queryLog['params']);
577 | }
578 |
579 | /**
580 | * 计算所使用的时间 毫秒
581 | *
582 | * @param int $start
583 | * @return float
584 | */
585 | protected function getElapsedTime($start)
586 | {
587 | return round((microtime(true) - $start) * 1000, 2);
588 | }
589 | }
--------------------------------------------------------------------------------
/src/Builder.php:
--------------------------------------------------------------------------------
1 | 0) {
60 | $this->setConnection(new Connection($config));
61 | }
62 | }
63 |
64 | /**
65 | * @return static
66 | */
67 | public function setAsGlobal()
68 | {
69 | static::$instance = $this;
70 | return $this;
71 | }
72 |
73 | /**
74 | * @param ContainerInterface $container
75 | */
76 | public static function setContainer($container)
77 | {
78 | static::$container = $container;
79 | }
80 |
81 | /**
82 | * @return static
83 | */
84 | public static function getInstance()
85 | {
86 | if (static::$instance instanceof Builder) {
87 | return static::$instance;
88 | }
89 |
90 | return static::$container->get('PFinal\Database\Builder');
91 | }
92 |
93 | /**
94 | * 设置数据库连接
95 | *
96 | * @return static
97 | */
98 | public function setConnection(Connection $connection)
99 | {
100 | $this->db = $connection;
101 | return $this;
102 | }
103 |
104 | /**
105 | * 返回数据库连接
106 | *
107 | * @return Connection
108 | */
109 | public function getConnection()
110 | {
111 | return $this->db;
112 | }
113 |
114 | /**
115 | * 指定查询表名
116 | *
117 | * 此方法将自动添加表前缀, 例如配置的表前缀为`cms_`, 则传入参数 `user` 将被替换为 `cms_user`, 等价于`{{%user}}`
118 | * 如果希望使用后缀, 例如表名为`user_cms`, 使用`{{user%}}`
119 | * 如果不希望添加表前缀,例如表名为`user`, 使用`{{user}}`
120 | * 如果使用自定义表前缀(不使用配置中指定的表前缀), 例如表前缀为`wp_`, 使用`{{wp_user}}`
121 | *
122 | * @param string $tableName 支持 as 例如 user as u
123 | * @return static
124 | */
125 | public function table($tableName = '', $asName = null)
126 | {
127 | if ($asName === null) {
128 | // 兼容as部份写在tableName中
129 | // user as u
130 | // {{user}} as u
131 | // {{%user}} as u
132 | // user u
133 | if (preg_match('/^(.+?)\s+(as\s+)?(\w+)$/i', $tableName, $res)) {
134 | $tableName = $res[1];
135 | $asName = $res[3];
136 | }
137 | }
138 |
139 | $builder = clone $this;
140 | $builder->table = self::addPrefix($tableName);
141 | $builder->tableAs = $this->addTableQuote($asName);
142 | return $builder;
143 | }
144 |
145 | /**
146 | * 添加表前缀
147 | *
148 | * @param $tableName
149 | * @return string
150 | * @throws Exception
151 | */
152 | public function addPrefix($tableName)
153 | {
154 | if (empty($tableName)) {
155 | return $tableName;
156 | }
157 | if (strpos($tableName, '{{') === false) {
158 | return $this->addTableQuote('%' . $tableName);
159 | }
160 |
161 | return $tableName;
162 | }
163 |
164 | private function addTableQuote($tableName)
165 | {
166 | if (empty($tableName)) {
167 | return $tableName;
168 | }
169 |
170 | if (strpos($tableName, '{{') === false) {
171 | return '{{' . $tableName . '}}';
172 | }
173 |
174 | if (!preg_match('/^\{\{%?[\w\-\.\$]+%?\}\}$/', $tableName)) {
175 | throw new Exception('表名含有不被允许的字符: ' . $tableName);
176 | }
177 |
178 | return $tableName;
179 | }
180 |
181 | /**
182 | * 执行新增
183 | *
184 | * @param array $data
185 | * @return bool
186 | */
187 | public function insert(array $data)
188 | {
189 | $names = array();
190 | $replacePlaceholders = array();
191 | foreach ($data as $name => $value) {
192 | static::checkColumnName($name);
193 | $names[] = '[[' . $name . ']]';
194 | $phName = ':' . $name;
195 | $replacePlaceholders[] = $phName;
196 | }
197 | $sql = 'INSERT INTO ' . $this->table . ' (' . implode(', ', $names) . ') VALUES (' . implode(', ', $replacePlaceholders) . ')';
198 | return 0 < static::getConnection()->execute($sql, $data);
199 | }
200 |
201 | /**
202 | * 执行新增,返回自增ID
203 | *
204 | * @param array $data
205 | * @return int
206 | */
207 | public function insertGetId(array $data)
208 | {
209 | if (static::insert($data)) {
210 | return static::getConnection()->getLastInsertId();
211 | }
212 | return 0;
213 | }
214 |
215 | /**
216 | * 根据SQL查询, 返回符合条件的所有数据, 没有结果时返回空数组
217 | *
218 | * @param string $sql
219 | * @param array $params
220 | * @return array
221 | */
222 | public function findAllBySql($sql = '', $params = array())
223 | {
224 | $sql = static::appendLock($sql);
225 |
226 | $useWritePdo = $this->useWritePdo;
227 | $fetchClass = $this->fetchClass;
228 |
229 | $afterFind = $this->afterFind;
230 |
231 | $this->reset();
232 |
233 | if ($fetchClass === null) {
234 | $fetchModel = array(\PDO::FETCH_ASSOC);
235 | } else {
236 | $fetchModel = array(\PDO::FETCH_CLASS, $fetchClass);
237 | }
238 |
239 | $data = static::getConnection()->query($sql, $params, $fetchModel, !$useWritePdo);
240 |
241 | if ($afterFind !== null) {
242 | call_user_func_array($afterFind, array($data));
243 | }
244 |
245 | return $data;
246 | }
247 |
248 | /**
249 | * 返回符合条件的所有数据, 没有结果时返回空数组
250 | *
251 | * @param string $condition
252 | * @param array $params
253 | * @return array
254 | */
255 | public function findAll($condition = '', $params = array())
256 | {
257 | $this->where($condition, $params);
258 |
259 | $sql = 'SELECT ' . static::getFieldString() . ' FROM ' . $this->table . $this->getTableAs()
260 | . $this->getJoinString()
261 | . $this->getWhereString()
262 | . $this->getGroupByString()
263 | . $this->getHavingByString()
264 | . $this->getOrderByString()
265 | . $this->getLimitString();
266 |
267 | $sql = static::replacePlaceholder($sql);
268 | return static::findAllBySql($sql, $this->params);
269 | }
270 |
271 | private function getTableAs()
272 | {
273 | if (empty($this->tableAs)) {
274 | return '';
275 | }
276 |
277 | return ' AS ' . $this->tableAs;
278 | }
279 |
280 | /**
281 | * 拆分查询,用于处理非常多的查询结果,而不会消耗大量内存,建议加上排序字段
282 | *
283 | * @param int $num 每次取出的数据数量 例如 100
284 | * @param callback $callback 每次取出数据时被调用,传入每次查询得到的数据(数组)
285 | *
286 | * 可以通过从闭包函数中返回 false 来中止组块的运行
287 | *
288 | * 示例
289 | *
290 | * DB::select('user')->where('status=1')->orderBy('id')->chunk(100, function ($users) {
291 | * foreach ($users as $user) {
292 | * // ...
293 | * }
294 | * });
295 | *
296 | * @return boolean
297 | */
298 | public function chunk($num, $callback)
299 | {
300 | $offset = 0;
301 | $limit = (int)$num;
302 | do {
303 | $query = clone $this;
304 | $query->offset($offset);
305 | $query->limit($limit);
306 | $data = $query->findAll();
307 | $offset += $limit;
308 |
309 | if (count($data) > 0) {
310 | if (call_user_func($callback, $data) === false) {
311 | return false;
312 | }
313 | }
314 | unset($query);
315 | } while (count($data) === $limit);
316 | $this->reset();
317 | return true;
318 | }
319 |
320 | /**
321 | * chunkById
322 | *
323 | * @param int $num
324 | * @param callable $callback
325 | * @param string $column
326 | * @return bool
327 | *
328 | * @see chunk
329 | */
330 | public function chunkById($num, callable $callback, $column = 'id')
331 | {
332 | $this->checkColumnName($column);
333 |
334 | $lastId = 0;
335 |
336 | do {
337 | $query = clone $this;
338 |
339 | $results = $query->where($column . ' > ?', array($lastId))
340 | ->orderBy($column)
341 | ->limit($num)
342 | ->findAll();
343 |
344 | $countResults = count($results);
345 |
346 | if ($countResults == 0) {
347 | break;
348 | }
349 |
350 | if ($callback($results) === false) {
351 | return false;
352 | }
353 |
354 | $last = end($results);
355 | $lastId = $last[$column];
356 |
357 | } while ($countResults == $num);
358 |
359 | return true;
360 | }
361 |
362 | // /**
363 | // * 游标迭代处理数据库记录, 执行查询并返回 Generator
364 | // *
365 | // * 使用示例:
366 | // *
367 | // * foreach (DB::table('user')->where('status=1')->cursor() as $user) {
368 | // * //
369 | // * }
370 | // *
371 | // * @return \Generator
372 | // */
373 | // public function cursor()
374 | // {
375 | // $sql = 'SELECT ' . static::getFieldString() . ' FROM ' . $this->table
376 | // . $this->getWhereString()
377 | // . $this->getOrderByString()
378 | // . $this->getLimitString();
379 | //
380 | // $sql = static::replacePlaceholder($sql);
381 | //
382 | // $params = $this->params;
383 | //
384 | // $sql = static::appendLock($sql);
385 | //
386 | // $useWritePdo = $this->useWritePdo;
387 | // $fetchClass = $this->fetchClass;
388 | //
389 | // $this->reset();
390 | //
391 | // if ($fetchClass === null) {
392 | // $fetchModel = array(\PDO::FETCH_ASSOC);
393 | // } else {
394 | // $fetchModel = array(\PDO::FETCH_CLASS, $fetchClass);
395 | // }
396 | //
397 | // return static::getConnection()->cursor($sql, $params, $fetchModel, !$useWritePdo);
398 | // }
399 |
400 | /**
401 | * 根据SQL返回对象, 没有结果时返回null
402 | *
403 | * @param string $sql
404 | * @param array $params
405 | * @return mixed
406 | */
407 | public function findOneBySql($sql = '', $params = array())
408 | {
409 | $rows = static::findAllBySql($sql, $params);
410 | if (count($rows) > 0) {
411 | return $rows[0];
412 | }
413 | return null;
414 | }
415 |
416 | /**
417 | * 返回符合条件的单条数据, 没有结果时返回null
418 | *
419 | * @param string $condition
420 | * @param array $params
421 | * @return mixed
422 | */
423 | public function findOne($condition = '', $params = array())
424 | {
425 | $this->limit = 1;
426 | $arr = $this->findAll($condition, $params);
427 | if (count($arr) == 0) {
428 | return null;
429 | }
430 | return $arr[0];
431 | }
432 |
433 | /**
434 | * @param string $condition
435 | * @param array $params
436 | * @return mixed
437 | */
438 | public function findOneOrFail($condition = '', $params = array())
439 | {
440 | $data = static::findOne($condition, $params);
441 | if ($data == null) {
442 | throw new NotFoundException('Data not found.');
443 | }
444 | return $data;
445 | }
446 |
447 | /**
448 | * 根据主键查询, 没有结果时返回null
449 | *
450 | * @param int|array $id 主键值
451 | * @param string|array $primaryKeyField 主键字段
452 | * @return mixed
453 | */
454 | public function findByPk($id, $primaryKeyField = null)
455 | {
456 | if (empty($id)) {
457 | return null;
458 | }
459 | $this->wherePk($id, $primaryKeyField);
460 | $this->limit = 1;
461 | return $this->findOne();
462 | }
463 |
464 | /**
465 | * 根据主键查询,没有记录时,抛出异常
466 | *
467 | * @param int|array $id
468 | * @param string|array $primaryKeyField
469 | * @return mixed
470 | * @throws NotFoundException
471 | * @see findByPk
472 | */
473 | public function findByPkOrFail($id, $primaryKeyField = null)
474 | {
475 | $data = static::findByPk($id, $primaryKeyField);
476 | if ($data == null) {
477 | throw new NotFoundException('Data not found: #' . $id);
478 | }
479 | return $data;
480 | }
481 |
482 | /**
483 | * 执行更新操作,返回受影响行数
484 | *
485 | * @param array $data 需要更新的数据, 关联数组,key为字段名,value为对应的值, 字段名只允许字母、数字或下划线
486 | * @param string $condition
487 | * @param array $params
488 | * @return int
489 | */
490 | public function update(array $data, $condition = '', $params = array())
491 | {
492 | $this->where($condition, $params);
493 |
494 | $updatePlaceholders = array();
495 | foreach ($data as $name => $value) {
496 | static::checkColumnName($name);
497 | $updatePlaceholders[] = "[[$name]]" . ' = ' . self::PARAM_PREFIX . $name;
498 | $this->params[self::PARAM_PREFIX . $name] = $value;
499 | }
500 |
501 | $sql = 'UPDATE ' . $this->table . ' SET ' . implode(', ', $updatePlaceholders) . $this->getWhereString();
502 | $sql = static::replacePlaceholder($sql);
503 |
504 | $rowCount = static::getConnection()->execute($sql, $this->params);
505 | $this->reset();
506 | return $rowCount;
507 | }
508 |
509 | /**
510 | * 自增 (如果自减,传入负数即可)
511 | *
512 | * @param string $field 字段
513 | * @param int|float $value 自增值,默认自增1
514 | * @param array $data 同时更新的其它字段值
515 | * @return int 回受影响行数
516 | */
517 | public function increment($field, $value = 1, $data = array())
518 | {
519 | static::checkColumnName($field);
520 |
521 | $updatePlaceholders = array();
522 | foreach ($data as $name => $val) {
523 | static::checkColumnName($name);
524 | $updatePlaceholders[] = "[[$name]]" . ' = ' . self::PARAM_PREFIX . $name;
525 | $this->params[self::PARAM_PREFIX . $name] = $val;
526 | }
527 |
528 | $updateStr = '';
529 | if (count($updatePlaceholders) > 0) {
530 | $updateStr = ', ' . implode(', ', $updatePlaceholders);
531 | }
532 |
533 | $sql = 'UPDATE ' . $this->table . ' SET [[' . $field . ']] = [[' . $field . ']] + ' . self::PARAM_PREFIX . '_increment' . $updateStr . $this->getWhereString();
534 | $this->params[self::PARAM_PREFIX . '_increment'] = $value;
535 |
536 | $sql = static::replacePlaceholder($sql);
537 |
538 | $rowCount = static::getConnection()->execute($sql, $this->params);
539 | $this->reset();
540 | return $rowCount;
541 | }
542 |
543 | /**
544 | * 执行删除操作,返回受影响行数
545 | *
546 | * @param string $condition
547 | * @param array $params
548 | * @return int
549 | */
550 | public function delete($condition = '', $params = array())
551 | {
552 | $this->where($condition, $params);
553 |
554 | $sql = 'DELETE FROM ' . $this->table . $this->getWhereString();
555 | $sql = static::replacePlaceholder($sql);
556 |
557 | $rowCount = static::getConnection()->execute($sql, $this->params);
558 | $this->reset();
559 | return $rowCount;
560 | }
561 |
562 | /**
563 | * 加载数据库字段默认值
564 | *
565 | * @param null $entity 对象,如果为空,此方法返回数组
566 | * @return array|object
567 | */
568 | public function loadDefaultValues($entity = null)
569 | {
570 | $fields = static::findAllBySql('SHOW FULL FIELDS FROM ' . $this->table);
571 | $defaults = array_column($fields, 'Default', 'Field');
572 |
573 | if ($entity === null) {
574 | return $defaults;
575 | }
576 |
577 | foreach ($defaults as $key => $value) {
578 | $entity->$key = $value;
579 | }
580 | return $entity;
581 | }
582 |
583 | /**
584 | * 分页获取数据
585 | * @param null $pageSize 每页数据条数
586 | * @param null $currentPage 当前页码,如果不指定,将自动从`$_GET['page']`获取
587 | * @return DataProvider
588 | */
589 | public function paginate($pageSize = null, $currentPage = null)
590 | {
591 | $pageConfig = array();
592 | if ($pageSize !== null) {
593 | $pageConfig['pageSize'] = intval($pageSize);
594 | }
595 | if ($currentPage !== null) {
596 | $pageConfig['currentPage'] = $currentPage;
597 | }
598 | return new DataProvider($this, $pageConfig);
599 | }
600 |
601 | /**
602 | * 统计查询 count()、sum()、max()、min()、avg()
603 | *
604 | * @param $method
605 | * @param $arguments
606 | * @return mixed
607 | */
608 | public function __call($method, $arguments)
609 | {
610 | if (!in_array(strtoupper($method), array('SUM', 'COUNT', 'MAX', 'MIN', 'AVG'))) {
611 | throw new Exception('Call to undefined method ' . __CLASS__ . '::' . $method . '()');
612 | }
613 |
614 | $field = isset($arguments[0]) ? $arguments[0] : '*';
615 |
616 | if (!($field instanceof Expression)) {
617 |
618 | $field = trim($field);
619 | if ($field !== '*') {
620 | if (!preg_match('/^[\w\.]+$/', $field)) {
621 | throw new Exception(__CLASS__ . '::' . $method . '() 第一个参数只允许字母、数字、下划线(_)、点(.) 或 星号(*)');
622 | }
623 | $field = '[[' . $field . ']]';
624 | }
625 | }
626 |
627 | $method = strtoupper($method);
628 |
629 | $sql = 'SELECT ' . $method . '(' . $field . ') FROM ' . $this->table . $this->getTableAs()
630 | . $this->getJoinString()
631 | . $this->getWhereString();
632 |
633 | $sql = static::replacePlaceholder($sql);
634 | $sql = static::appendLock($sql);
635 | $result = static::getConnection()->queryScalar($sql, $this->params, !$this->useWritePdo);
636 | $this->reset();
637 | return $result;
638 | }
639 |
640 | /**
641 | * 限制查询返回记录条数
642 | *
643 | * @param int|string $limit 为string时,可以时指定offset,例如"20,10"
644 | * @return $this
645 | */
646 | public function limit($limit)
647 | {
648 | if (is_string($limit) && strpos($limit, ',') !== false) {
649 | list($this->offset, $limit) = explode(',', $limit);
650 | }
651 | $this->limit = trim($limit);
652 | return $this;
653 | }
654 |
655 | /**
656 | * 设置查询跳过记录数
657 | *
658 | * @param int $offset
659 | * @return $this
660 | */
661 | public function offset($offset)
662 | {
663 | $this->offset = $offset;
664 | return $this;
665 | }
666 |
667 | /**
668 | * 排序
669 | *
670 | * MySQL随机排序 orderBy(new \PFinal\Database\Expression('rand()'))
671 | *
672 | * @param array|string $columns
673 | * array: ['age' => SORT_ASC, 'id' => SORT_DESC]
674 | * string: "age, id desc"
675 | * @return $this
676 | */
677 | public function orderBy($columns)
678 | {
679 | $this->orderBy = $this->normalizeOrderBy($columns);
680 | return $this;
681 | }
682 |
683 | /**
684 | * 设置条件
685 | *
686 | * @param string|array $condition 条件 例如 `name=? AND status=?` 或者 `['name'=>'Ethan', 'status'=>1]`, 为数组时字段之间使用AND连接
687 | * @param array $params 条件中占位符对应的值。 当`$condition`为`array`时,此参数无效
688 | * @param bool $andWhere 重复调用`where()`时, 默认使用`AND`与已有条件连接, 此参数为`false`时, 使用`OR`连接有条件
689 | * @return static
690 | */
691 | public function where($condition = '', $params = array(), $andWhere = true)
692 | {
693 | if (static::isEmpty($condition)) {
694 | return $this;
695 | }
696 |
697 | if (is_array($condition)) {
698 | return $this->whereWithArray($condition, true, $andWhere);
699 | }
700 |
701 | if (is_object($params) && method_exists($params, '__toString')) {
702 | $params = $params->__toString();
703 | }
704 |
705 | if (!is_array($params)) {
706 | $params = array($params); //防止传入单个值时未使用数组类型
707 | }
708 |
709 | if (empty($this->condition)) {
710 | $this->condition = $condition;
711 | $this->params = $params;
712 | } else {
713 | $glue = $andWhere ? ' AND ' : ' OR ';
714 | $this->condition = '(' . $this->condition . ')' . $glue . '(' . $condition . ')';
715 | $this->params = array_merge($this->params, $params);
716 | }
717 | return $this;
718 | }
719 |
720 | /**
721 | * 主键作为条件
722 | *
723 | * @param int|array $id 主键的值
724 | * @param string|array $primaryKeyField 主键字段名,如果不传,则自动获取
725 | * @return static
726 | */
727 | public function wherePk($id, $primaryKeyField = null)
728 | {
729 | if ($primaryKeyField == null) {
730 | $primaryKeyField = self::primaryKeyFields();
731 | }
732 |
733 | return $this->where(array_combine((array)$primaryKeyField, (array)$id));
734 | }
735 |
736 | /**
737 | * inner Join
738 | *
739 | * @param string $table 表名,例如 "user as u"
740 | * @param string $on
741 | * @return $this
742 | */
743 | public function join($table, $on)
744 | {
745 | $asName = '';
746 | if (preg_match('/^(.+?)\s+(as\s+)?(\w+)$/i', $table, $res)) {
747 | $table = $res[1];
748 | $asName = $res[3];
749 | $asName = ' AS ' . self::addTableQuote($asName);
750 | }
751 |
752 | $type = 'JOIN';
753 |
754 | $table = self::addPrefix($table) . $asName;
755 | $this->join[] = compact('type', 'table', 'on');
756 | return $this;
757 | }
758 |
759 | /**
760 | * left Join
761 | *
762 | * @param string $table 表名,例如 "user as u"
763 | * @param string $on
764 | * @return $this
765 | */
766 | public function leftJoin($table, $on)
767 | {
768 | $asName = '';
769 | if (preg_match('/^(.+?)\s+(as\s+)?(\w+)$/i', $table, $res)) {
770 | $table = $res[1];
771 | $asName = $res[3];
772 | $asName = ' AS ' . self::addTableQuote($asName);
773 | }
774 |
775 | $type = 'LEFT JOIN';
776 | $table = self::addPrefix($table) . $asName;
777 | $this->join[] = compact('type', 'table', 'on');
778 | return $this;
779 | }
780 |
781 | /**
782 | * group by
783 | *
784 | * @param $groupBy
785 | * @return $this
786 | */
787 | public function groupBy($groupBy)
788 | {
789 | $this->groupBy = $groupBy;
790 | return $this;
791 | }
792 |
793 | /**
794 | * having
795 | *
796 | * @param string $having 占位符目前只支持冒号格式 例如 having('account_id > : account_id', ['account_id'>100])
797 | * @param array $params
798 | * @return $this
799 | */
800 | public function having($having, array $params = array())
801 | {
802 | //占位符目前只支持冒号格式,检测是否有问号
803 | if (strpos($having, '?') !== false) {
804 | throw new Exception('having cannot contain a question mark');
805 | }
806 |
807 | $this->having = compact('having', 'params');
808 | return $this;
809 | }
810 |
811 | /**
812 | * 主键字段
813 | *
814 | * @return array 例如 ['id']
815 | */
816 | private function primaryKeyFields()
817 | {
818 | $fields = static::schema();
819 |
820 | $primary = array();
821 | foreach ($fields as $field) {
822 | if ($field['Key'] === 'PRI') {
823 | $primary[] = $field['Field'];
824 | }
825 | }
826 |
827 | return $primary;
828 | }
829 |
830 | private static $schemas = array();
831 |
832 | /**
833 | * @return array
834 | */
835 | private function schema()
836 | {
837 | if (!array_key_exists($this->table, static::$schemas)) {
838 | static::$schemas[$this->table] = $this->getConnection()->query('SHOW FULL FIELDS FROM ' . $this->table);
839 | }
840 | return static::$schemas[$this->table];
841 | }
842 |
843 | /**
844 | * 设置条件(IN查询) 例如 `whereIn('id', [1, 2, 3])`
845 | *
846 | * @param string $field 字段
847 | * @param array $values 条件值, 索引数组
848 | * @param bool $andWhere 对应where()方法条三个参数
849 | * @return $this
850 | */
851 | public function whereIn($field, array $values, $andWhere = true)
852 | {
853 | self::checkColumnName($field);
854 |
855 | if (count($values) == 0) {
856 | //in条件为空, 给一个值为false的条件,避免查询到任何结果
857 | return $this->where('1 != 1');
858 | }
859 |
860 | $values = array_values($values);
861 |
862 | //如果不是类似 "user.id",则加上转义 "`id`",防止列名是关键字的情况
863 | if (strpos($field, '.') === false) {
864 | $field = '[[' . $field . ']]';
865 | }
866 |
867 | return $this->where($field . ' IN (' . rtrim(str_repeat('?,', count($values)), ',') . ')', $values, $andWhere);
868 | }
869 |
870 | /**
871 | * 指定查询返回的类名, 默认情况下`findAll()`以关联数组格式返回结果,调用`asEntity()`指定类名后,查询将以对象返回
872 | *
873 | * 默认情况下 `findAll()`方法返回:
874 | * [
875 | * ['id'=>1, 'name'=>'Jack'],
876 | * ['id'=>2, 'name'=>'Mary']
877 | * ]
878 | *
879 | * 指定返回类名之后, asEntity('User')->findAll() 方法将返回:
880 | * [
881 | * object(User) public 'id'=>1, 'name'=>'Jack',
882 | * object(User) public 'id'=>2, 'name'=>'Mary'
883 | * ]
884 | *
885 | * @param $className
886 | * @return $this
887 | */
888 | public function asEntity($className)
889 | {
890 | $this->fetchClass = $className;
891 | return $this;
892 | }
893 |
894 | /**
895 | * 更新锁可避免行被其它共享锁修改或选取 (在事务中有效)
896 | *
897 | * @return $this
898 | */
899 | public function lockForUpdate()
900 | {
901 | $this->lockForUpdate = true;
902 | $this->useWritePdo();
903 | return $this;
904 | }
905 |
906 | /**
907 | * 共享锁(sharedLock) 可防止选中的数据被篡改,直到事务被提交为止 (在事务中有效)
908 | *
909 | * @return $this
910 | */
911 | public function lockInShareMode()
912 | {
913 | $this->lockInShareMode = true;
914 | $this->useWritePdo();
915 | return $this;
916 | }
917 |
918 | /**
919 | * @return $this
920 | * @see lockInShareMode
921 | */
922 | public function sharedLock()
923 | {
924 | return $this->lockInShareMode();
925 | }
926 |
927 | /**
928 | * 指定查询字段 推荐使用数组,例如 ['id','name','age']
929 | * @param array|string $field
930 | * @return $this
931 | */
932 | public function field($field)
933 | {
934 | $this->field = $field;
935 | return $this;
936 | }
937 |
938 | /**
939 | * 在一个 try/catch 块中执行给定的回调,如果回调用没有抛出任何异常,将自动提交事务
940 | *
941 | * 如果捕获到任何异常, 将自动回滚事务后,继续抛出异常
942 | *
943 | * @param \Closure $callback
944 | * @return mixed
945 | *
946 | * @throws \Throwable
947 | */
948 | public function transaction(Closure $callback)
949 | {
950 | try {
951 |
952 | $this->getConnection()->beginTransaction();
953 | $result = $callback($this);
954 | $this->getConnection()->commit();
955 | return $result;
956 |
957 | } catch (\Exception $ex) { //PHP 5.x
958 |
959 | $this->getConnection()->rollBack();
960 | throw $ex; //回滚事务后继续向外抛出异常,让开发人员自行处理后续操作
961 |
962 | } catch (\Throwable $ex) { //PHP 7
963 |
964 | $this->getConnection()->rollBack();
965 | throw $ex;
966 | }
967 | }
968 |
969 | /**
970 | * 返回sql语句,用于调试 select 语句
971 | *
972 | * @return string
973 | */
974 | public function toSql()
975 | {
976 | $sql = 'SELECT ' . static::getFieldString() . ' FROM ' . $this->table . $this->getTableAs()
977 | . $this->getJoinString()
978 | . $this->getWhereString()
979 | . $this->getGroupByString()
980 | . $this->getHavingByString()
981 | . $this->getOrderByString()
982 | . $this->getLimitString();
983 |
984 | $sql = static::replacePlaceholder($sql);
985 | $conn = $this->getConnection();
986 | $sql = $conn->parsePlaceholder($conn->quoteSql($sql), $this->params);
987 | $this->reset();
988 | return $sql;
989 | }
990 |
991 | /**
992 | * group by
993 | *
994 | * @return string
995 | */
996 | protected function getGroupByString()
997 | {
998 | if (static::isEmpty($this->groupBy)) {
999 | return '';
1000 | }
1001 | return ' GROUP BY ' . $this->groupBy;
1002 | }
1003 |
1004 | /**
1005 | * having
1006 | *
1007 | * @return string
1008 | */
1009 | protected function getHavingByString()
1010 | {
1011 | if (static::isEmpty($this->having)) {
1012 | return '';
1013 | }
1014 |
1015 | if (!static::isEmpty($this->having['params'])) {
1016 | $this->params = array_merge($this->params, $this->having['params']);
1017 | }
1018 |
1019 | return ' HAVING ' . $this->having['having'];
1020 | }
1021 |
1022 | /**
1023 | * 处理数组条件
1024 | *
1025 | * @param array $where
1026 | * @param bool $andGlue 是否使用AND连接数组中的多个成员
1027 | * @param bool $andWhere 重复调用`where()`时, 默认使用`AND`与已有条件连接, 此参数为`false`时, 使用`OR`连接有条件
1028 | * @return $this
1029 | */
1030 | protected function whereWithArray(array $where, $andGlue = true, $andWhere = true)
1031 | {
1032 | if (static::isEmpty($where)) {
1033 | return $this;
1034 | }
1035 | $params = array();
1036 | $conditions = array();
1037 | foreach ($where as $k => $v) {
1038 | static::checkColumnName($k);
1039 | $conditions[] = '[[' . $k . ']] = ?';
1040 | $params[] = $v;
1041 | }
1042 | $glue = $andGlue ? ' AND ' : ' OR ';
1043 | return $this->where(join($glue, $conditions), $params, $andWhere);
1044 | }
1045 |
1046 | /**
1047 | * 规范为数组格式
1048 | *
1049 | * @param array|string $columns
1050 | * @return array
1051 | */
1052 | protected function normalizeOrderBy($columns)
1053 | {
1054 | if ($columns instanceof Expression) {
1055 | return $columns;
1056 | }
1057 |
1058 | // ['age' => SORT_ASC, 'id' => SORT_DESC]
1059 | if (is_array($columns)) {
1060 | return $columns;
1061 | }
1062 |
1063 | if (static::isEmpty($columns)) {
1064 | return null;
1065 | }
1066 |
1067 | $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
1068 |
1069 | $result = array();
1070 | foreach ($columns as $column) {
1071 | if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
1072 | $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC;
1073 | } else {
1074 | $result[$column] = SORT_ASC;
1075 | }
1076 | }
1077 |
1078 | return $result;
1079 | }
1080 |
1081 | /**
1082 | * 返回 join 语句
1083 | * @return string
1084 | */
1085 | protected function getJoinString()
1086 | {
1087 | if (static::isEmpty($this->join)) {
1088 | return '';
1089 | }
1090 |
1091 | $join = array();
1092 |
1093 | foreach ($this->join as $value) {
1094 | $join[] = $value['type'] . ' ' . $value['table'] . ' ON ' . $value['on'];
1095 | }
1096 |
1097 | return ' ' . join(' ', $join);
1098 | }
1099 |
1100 | /**
1101 | * 返回where部份sql
1102 | *
1103 | * @return string
1104 | */
1105 | protected function getWhereString()
1106 | {
1107 | return static::isEmpty($this->condition) ? '' : (' WHERE ' . $this->condition);
1108 | }
1109 |
1110 | /**
1111 | * 返回字段部份sql
1112 | *
1113 | * @return array|string
1114 | * @throws Exception
1115 | */
1116 | protected function getFieldString()
1117 | {
1118 | $field = $this->field;
1119 |
1120 | if ($field instanceof Expression) {
1121 | return $field;
1122 | }
1123 |
1124 | $return = '*';
1125 | if (!static::isEmpty($field)) {
1126 | if (is_array($field)) {
1127 | $return = array();
1128 | foreach ($field as $value) {
1129 | $return[] = '[[' . $value . ']]';
1130 | }
1131 | $return = join(',', $return);
1132 | } else {
1133 | $return = $field;
1134 | }
1135 | if (!preg_match('/^[\w\s\.\,\[\]`\*]+$/', $return)) {
1136 | throw new Exception('字段名含有不被允许的字符');//字母、数字、下划线、空白、点、星号、逗号、中括号、反引号
1137 | }
1138 | }
1139 | return $return;
1140 | }
1141 |
1142 | /**
1143 | * 返回排序部份sql
1144 | *
1145 | * @return string
1146 | */
1147 | protected function getOrderByString()
1148 | {
1149 | $orderBy = $this->orderBy;
1150 | if ($orderBy !== null) {
1151 |
1152 | if ($orderBy instanceof Expression) {
1153 | return ' ORDER BY ' . $orderBy;
1154 | }
1155 |
1156 | $orders = array();
1157 | foreach ($orderBy as $name => $direction) {
1158 | static::checkColumnName($name);
1159 | $orders[] = $name . ($direction === SORT_DESC ? ' DESC' : '');
1160 | }
1161 | return ' ORDER BY ' . implode(', ', $orders);
1162 | }
1163 | return '';
1164 | }
1165 |
1166 | /**
1167 | * 返回limit部份sql
1168 | *
1169 | * @return string
1170 | * @throws Exception
1171 | */
1172 | protected function getLimitString()
1173 | {
1174 | $limit = trim($this->limit);
1175 | $offset = trim($this->offset);
1176 |
1177 | if (static::isEmpty($limit)) {
1178 | return '';
1179 | }
1180 |
1181 | if (static::isEmpty($offset)) {
1182 | $offset = 0;
1183 | }
1184 |
1185 | if (preg_match('/^\d+$/', $limit) && preg_match('/^\d+$/', $offset)) {
1186 | if ($offset == 0) {
1187 | return ' LIMIT ' . $limit;
1188 | } else {
1189 | return ' LIMIT ' . $offset . ', ' . $limit;
1190 | }
1191 | }
1192 | throw new Exception("offset 或 limit 含有不被允许的字符");
1193 | }
1194 |
1195 | /**
1196 | * lock
1197 | *
1198 | * @param $sql
1199 | * @return string
1200 | */
1201 | protected function appendLock($sql)
1202 | {
1203 | if ($this->lockForUpdate === true) {
1204 | $sql = rtrim($sql) . ' FOR UPDATE';
1205 | } else if ($this->lockInShareMode === true) {
1206 | $sql = rtrim($sql) . ' LOCK IN SHARE MODE';
1207 | }
1208 |
1209 | return $sql;
1210 | }
1211 |
1212 | /**
1213 | * 检查列名是否有效
1214 | *
1215 | * @param string $column 列名只允许字母、数字、下划线、点(.)、中杠(-)
1216 | * @throws Exception
1217 | */
1218 | protected static function checkColumnName($column)
1219 | {
1220 | if (!preg_match('/^[\w\-\.]+$/', $column)) {
1221 | throw new Exception('列名含有不被允许的字符');//只允许字母、数字、下划线、点(.)、中杠(-)
1222 | }
1223 | }
1224 |
1225 | /**
1226 | * 统一占位符 如果同时存在问号和冒号,则将问号参数转为冒号
1227 | *
1228 | * @param $sql
1229 | * @return string
1230 | */
1231 | protected function replacePlaceholder($sql)
1232 | {
1233 | static $staticCount = 0;
1234 | if (strpos($sql, '?') !== false && strpos($sql, ':') !== false) {
1235 | $count = substr_count($sql, '?');
1236 | for ($i = 0; $i < $count; $i++) {
1237 | $num = $i + $staticCount;
1238 | $staticCount++;
1239 | $sql = preg_replace('/\?/', static::PARAM_PREFIX . $num, $sql, 1);
1240 | $this->params[static::PARAM_PREFIX . $num] = $this->params[$i];
1241 | unset($this->params[$i]);
1242 | }
1243 | }
1244 | return $sql;
1245 | }
1246 |
1247 | /**
1248 | * 检查是否为空 以下值: null、''、空数组、空白字符("\t"、"\n"、"\r"等) 被为认为是空值
1249 | *
1250 | * @param mixed $value
1251 | * @return boolean
1252 | */
1253 | protected static function isEmpty($value)
1254 | {
1255 | return $value === '' || $value === array() || $value === null || is_string($value) && trim($value) === '';
1256 | }
1257 |
1258 | /**
1259 | * 在查询操作中,默认使用从库,调用此方法后,将强制使用主库做查询
1260 | *
1261 | * @return $this
1262 | */
1263 | public function useWritePdo()
1264 | {
1265 | $this->useWritePdo = true;
1266 | return $this;
1267 | }
1268 |
1269 | protected $afterFind;
1270 |
1271 | /**
1272 | * 查询之后的处理函数,对每个查询得到的结果应用此函数
1273 | *
1274 | * @param $callback
1275 | * @return $this
1276 | * @throws Exception
1277 | */
1278 | public function afterFind($callback)
1279 | {
1280 | if (!is_callable($callback)) {
1281 | throw new Exception('$callback is not a callable');
1282 | }
1283 | $this->afterFind = $callback;
1284 |
1285 | return $this;
1286 | }
1287 |
1288 | /**
1289 | * 清空所有条件
1290 | */
1291 | protected function reset()
1292 | {
1293 | $this->table = null;
1294 | $this->tableAs = null;
1295 | $this->fetchClass = null;
1296 | $this->orderBy = null;
1297 | $this->field = null;
1298 | $this->limit = null;
1299 | $this->offset = null;
1300 | $this->condition = null;
1301 | $this->params = array();
1302 | $this->lockForUpdate = null;
1303 | $this->lockInShareMode = null;
1304 | $this->useWritePdo = false;
1305 | $this->afterFind = null;
1306 | $this->join = array();
1307 | $this->groupBy = null;
1308 | $this->having = null;
1309 | }
1310 | }
--------------------------------------------------------------------------------