├── .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}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}>...disabledTag}>" . $ctrl_num_html; 155 | } 156 | if ($ctrl_end != $this->pageCount) { 157 | $url = $this->createPageLink($this->pageCount); 158 | $ctrl_num_html .= "<{$this->disabledTag}>...disabledTag}>{$this->pageCount}"; 159 | } 160 | 161 | //上一页 162 | if ($this->currentPage == 1) { 163 | $prev = "<{$this->disabledTag} class='{$this->prevPageCssClass} {$this->disabledCssClass}'>{$this->prevPageLabel}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}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 | } --------------------------------------------------------------------------------