├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── README.md ├── composer.json ├── phpunit.php ├── src ├── AbstractModel.php ├── ConnectionConfig.php ├── Db │ ├── MysqlClient.php │ ├── Pool.php │ └── QueryResult.php ├── DbManager.php ├── Exception │ ├── Exception.php │ ├── ExecuteFail.php │ ├── ModelError.php │ ├── PoolError.php │ └── PrepareFail.php ├── QueryExecutor.php ├── RuntimeCache.php └── RuntimeConfig.php └── usage.md /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## easyswoole框架版本号、orm组件版本号 [Version] 2 | 3 | ## 问题描述和截图 [Question] 4 | 5 | ## 排查情况和最小复现脚本 [Tests and Recurrence] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .idea/ 4 | siam/ 5 | .vscode/ 6 | .project 7 | .settings 8 | .buildpath -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Easyswoole-ORM 2 | 3 | ## 项目背景 4 | 5 | 由于swoole协程环境不可以直接使用php-fpm的orm组件(由于存在静态全局变量、连接层没有做好协程处理,无法协程安全地使用) 6 | 7 | 所以easyswoole花费大量时间精力维护orm组件,连贯操作等功能设计借鉴TP5.0的ORM组件。 8 | 9 | 有疑问、功能建议、bug反馈请在QQ群、github issue、直接联系宣言提交。 10 | 11 | ## 问题反馈模板 12 | 13 | 一个完整的提问需要包含以下几点: 14 | 15 | - 1.出现了问题,怀疑orm组件的bug,需要先行编写最小测试。(比如一个新的类,单独只调用一个功能,都出问题了,排除其他因素影响) 16 | - 2.用文字描述出现的问题,附带运行和调试的参数截图 17 | - 3.附带第一步最小测试复现脚本 18 | 19 | ## 安装 20 | 21 | ``` 22 | composer require easyswoole/orm 23 | ``` 24 | 25 | ## RFC 26 | ### 1、Model Invoke 27 | ``` 28 | Model::invoke()->where(col,val)->get() 29 | Model::invoke(function(Model $m){ 30 | $m->where(col,val) 31 | })->get() 32 | ``` 33 | 34 | ### 2、Model Where 35 | ``` 36 | $model = Model::create() 37 | 38 | //$op => = , > , < , != , in , between 39 | $model->where(col1,val1,$op)->get() 40 | $model->where(col1,100,">")->get() 41 | $model->where(col1,100,"<")->get() 42 | $model->where(col1,100,"=")->get() 43 | $model->where(col1,[1,100],"in")->get() 44 | $model->where(col1,[1,100],"between")->get() 45 | 46 | $model->where(col1,val1)->where(col2,val2)->get() 47 | => select * from where col1 = val1 and col2 = val2 limit 1 48 | 49 | 50 | // 数组形式传参 仅支持此种格式数组 51 | $array1 = [ 52 | ['user', 'easyswoole', '='], 53 | ['age', '18', '!='], 54 | ]; 55 | $array1 = [ 56 | ['creaet_time', '2021-12-29 14:47:32', '='], 57 | ]; 58 | $model->where($array1)->where($array2)->get() 59 | 60 | 61 | // TODO 兼容 原生sql 62 | 63 | ``` 64 | 65 | ## 官网文档 66 | 67 | http://www.easyswoole.com/Cn/Components/Orm/changeLog.html 68 | 69 | ## 单元测试 70 | 71 | ```php 72 | ./vendor/bin/co-phpunit tests 73 | ``` 74 | 75 | 推荐使用mysql著名的employees样例库进行测试和学习mysql: https://github.com/datacharmer/test_db 76 | 77 | ## 主要项目负责人 78 | 79 | 80 | 81 | ## 参与贡献方式 82 | 83 | - 有实际生产使用的,提出升级迭代建议 84 | - 使用过程中遇到问题,并且查看文档,基本排除个人原因导致的问题,怀疑bug,及时反馈 85 | - 参与orm组件的代码维护、功能升级 86 | - 参与orm组件的文档维护(也就是加入easyswoole文档维护团队) 87 | 88 | ## 开源协议 89 | 90 | Apache-2.0 91 | 92 | ## 功能介绍 93 | 94 | - 基于easyswoole/mysqli组件,承当构造层职责。 95 | - 基于easyswoole/pool组件,承当基础连接池。 96 | - 支持执行自定义sql语句、构造器查询。 97 | - 支持事务,DbManager连接管理器也可承当事务管理器的职责。 98 | - 支持关联查询。 99 | - 支持多数据库配置,读写分离。 100 | - 便捷的连贯操作、聚合操作。 101 | 102 | ## 设计层级 103 | 104 | ![设计层级](http://www.easyswoole.com/Images/Orm/%E8%AE%BE%E8%AE%A1%E5%B1%82%E7%BA%A7.svg) 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easyswoole/orm", 3 | "type": "library", 4 | "description": "php stander lib", 5 | "keywords": [ 6 | "swoole", 7 | "framework", 8 | "async", 9 | "easyswoole" 10 | ], 11 | "homepage": "https://www.easyswoole.com/", 12 | "license": "Apache-2.0", 13 | "authors": [ 14 | { 15 | "name": "YF", 16 | "email": "291323003@qq.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.1.0", 21 | "ext-swoole": ">=4.4.7", 22 | "ext-json": "*", 23 | "easyswoole/component": "^2.0", 24 | "easyswoole/mysqli": "^3.0", 25 | "easyswoole/ddl": "2.x", 26 | "easyswoole/pool": "^1.1" 27 | }, 28 | "require-dev": { 29 | "easyswoole/phpunit": "^1.0", 30 | "easyswoole/swoole-ide-helper": "^1.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "EasySwoole\\ORM\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "EasySwoole\\ORM\\Tests\\": "tests/" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 5 | 'port' => 3306, 6 | 'user' => 'demo', 7 | 'password' => '123456', 8 | 'database' => 'demo', 9 | 'timeout' => 5, 10 | 'charset' => 'utf8mb4', 11 | ]); -------------------------------------------------------------------------------- /src/AbstractModel.php: -------------------------------------------------------------------------------- 1 | data($data); 31 | } 32 | } 33 | 34 | public static function create(?array $data = null):AbstractModel 35 | { 36 | return new static($data); 37 | } 38 | 39 | public function data(array $data, $setter = true) 40 | { 41 | foreach ($data as $key => $value) { 42 | $this->setAttr($key, $value, $setter); 43 | } 44 | return $this; 45 | } 46 | 47 | function runtimeConfig(?RuntimeConfig $config = null):RuntimeConfig 48 | { 49 | if($config == null){ 50 | if($this->runtimeConfig == null){ 51 | $this->runtimeConfig = new RuntimeConfig(); 52 | } 53 | }else{ 54 | $this->runtimeConfig = $config; 55 | } 56 | return $this->runtimeConfig; 57 | } 58 | 59 | function lastQueryResult():?QueryResult 60 | { 61 | return $this->__lastQueryResult; 62 | } 63 | 64 | function schemaInfo():Table 65 | { 66 | $key = $this->__modelhash(); 67 | $item = RuntimeCache::getInstance()->get($key); 68 | if($item){ 69 | return $item; 70 | } 71 | $client = $this->runtimeConfig()->getClient(); 72 | $query = new QueryBuilder(); 73 | $query->raw("show full columns from {$this->tableName()}"); 74 | 75 | $fields = DbManager::getInstance() 76 | ->__exec($client,$query,false,$this->runtimeConfig()->getConnectionConfig()->getTimeout()) 77 | ->getResult(); 78 | $table = new Table($this->tableName()); 79 | 80 | foreach ($fields as $field){ 81 | //创建字段与类型处理 82 | $columnTypeArr = explode(' ',$field['Type']); 83 | $tmpIndex = strpos($columnTypeArr[0],'('); 84 | //例如 varchar(20) 85 | if($tmpIndex !== false){ 86 | $type = substr($columnTypeArr[0],0,$tmpIndex); 87 | $limit = substr($columnTypeArr[0],$tmpIndex+1,strpos($columnTypeArr[0],')')-$tmpIndex-1); 88 | $columnObj = new Column($field['Field'],$type); 89 | $limitArr = explode(',',$limit); 90 | if (isset($limitArr[1])){ 91 | $columnObj->setColumnLimit($limitArr); 92 | }else{ 93 | $columnObj->setColumnLimit($limitArr[0]); 94 | } 95 | }else{ 96 | $type = $columnTypeArr[0]; 97 | $columnObj = new Column($field['Field'],$type); 98 | } 99 | if (in_array('unsigned',$columnTypeArr)){ 100 | $columnObj->setIsUnsigned(); 101 | } 102 | if ($field['Key']=='PRI'){ 103 | $columnObj->setIsPrimaryKey(); 104 | } 105 | //默认值 106 | if ($field['Default']!==null){ 107 | $columnObj->setDefaultValue($field['Default']); 108 | }else{ 109 | $columnObj->setDefaultValue(null); 110 | } 111 | if ($field['Extra']=='auto_increment'){ 112 | $columnObj->setIsAutoIncrement(); 113 | } 114 | if (!empty($field['Comment'])){ 115 | $columnObj->setColumnComment($field['Comment']); 116 | } 117 | $table->addColumn($columnObj); 118 | } 119 | 120 | RuntimeCache::getInstance()->set($key,$table); 121 | 122 | return $table; 123 | } 124 | 125 | public function offsetExists($offset): bool 126 | { 127 | return $this->__isset($offset); 128 | } 129 | 130 | public function offsetGet($offset) 131 | { 132 | return $this->getAttr($offset); 133 | } 134 | 135 | public function offsetSet($offset, $value) 136 | { 137 | return $this->setAttr($offset, $value); 138 | } 139 | 140 | public function offsetUnset($offset) 141 | { 142 | return $this->setAttr($offset, null); 143 | } 144 | 145 | 146 | function __set($name, $value) 147 | { 148 | //访问的时候,恢复ddl定义的默认值 149 | if($this->__data === null){ 150 | $this->__data = $this->__tableArray(); 151 | } 152 | $this->setAttr($name, $value); 153 | } 154 | 155 | function __get($name) 156 | { 157 | //访问的时候,恢复ddl定义的默认值 158 | if($this->__data === null){ 159 | $this->__data = $this->__tableArray(); 160 | } 161 | return $this->getAttr($name); 162 | } 163 | 164 | public function __isset($name) 165 | { 166 | return ($this->getAttr($name) !== null); 167 | } 168 | 169 | public function getAttr($attrName) 170 | { 171 | $method = 'get' . str_replace( ' ', '', ucwords( str_replace( ['-', '_'], ' ', $attrName ) ) ) . 'Attr'; 172 | if (method_exists($this, $method)) { 173 | return $this->$method($this->data[$attrName] ?? null, $this->data); 174 | } 175 | // 判断是否有关联查询 176 | if (method_exists($this, $attrName)) { 177 | return $this->$attrName(); 178 | } 179 | 180 | return $this->__data[$attrName] ?? null; 181 | } 182 | 183 | public function setAttr($attrName, $attrValue, $setter = true): bool 184 | { 185 | if (isset($this->schemaInfo()->getColumns()[$attrName])) { 186 | /** @var Column $col */ 187 | $col = $this->schemaInfo()->getColumns()[$attrName]; 188 | if(DataType::typeIsTextual($col->getColumnType())){ 189 | $attrValue = strval($attrValue); 190 | } 191 | $method = 'set' . str_replace( ' ', '', ucwords( str_replace( ['-', '_'], ' ', $attrName ) ) ) . 'Attr'; 192 | if ($setter && method_exists($this, $method)) { 193 | $attrValue = $this->$method($attrValue, $this->__data); 194 | } 195 | $this->__data[$attrName] = $attrValue; 196 | return true; 197 | } else { 198 | return false; 199 | } 200 | } 201 | 202 | public function where(...$args) 203 | { 204 | $this->runtimeConfig()->where($args); 205 | return $this; 206 | } 207 | 208 | public function join($joinTable, $joinCondition, $joinType = '') 209 | { 210 | $this->runtimeConfig()->join([ 211 | $joinTable,$joinCondition,$joinType 212 | ]); 213 | return $this; 214 | } 215 | 216 | public function order(...$args) 217 | { 218 | $this->runtimeConfig()->order($args); 219 | return $this; 220 | } 221 | 222 | public function limit(int $one, ?int $two = null) 223 | { 224 | $this->runtimeConfig()->limit($one,$two); 225 | return $this; 226 | } 227 | 228 | public function field($fields) 229 | { 230 | $this->runtimeConfig()->field($fields); 231 | return $this; 232 | } 233 | 234 | public function groupBy($filed) 235 | { 236 | $this->runtimeConfig()->groupBy($filed); 237 | return $this; 238 | } 239 | 240 | public function withTotalCount() 241 | { 242 | $this->runtimeConfig()->withTotalCount(); 243 | return $this; 244 | } 245 | 246 | public function findOne($pkVal = null):?AbstractModel 247 | { 248 | if($pkVal !== null){ 249 | $pkName = $this->__tablePk(); 250 | $this->where($pkName,$pkVal); 251 | } 252 | $this->limit(1); 253 | $builder = $this->__makeBuilder(); 254 | $builder->get($this->tableName()); 255 | $data = $this->__exec($builder); 256 | if($data){ 257 | return new static($data[0]); 258 | } 259 | return null; 260 | } 261 | 262 | protected function mapOne(string $targetModelClass,$targetModeWhereCol,?string $currentModeWhereCol = null,string $joinType = "left") 263 | { 264 | $target = new \ReflectionClass($targetModelClass); 265 | if(!$target->isSubclassOf(AbstractModel::class)){ 266 | throw new ExecuteFail("{$targetModelClass} not a subclass of ".AbstractModel::class); 267 | } 268 | if($currentModeWhereCol === null){ 269 | $currentModeWhereCol = $this->__tablePk(); 270 | } 271 | /** @var AbstractModel $target */ 272 | $target = $target->newInstance(); 273 | $builder = new QueryBuilder(); 274 | $preData = $this->runtimeConfig()->getPreQueryData(); 275 | if(!empty($preData)){ 276 | $target->data($preData[$targetModelClass]); 277 | return $target; 278 | } 279 | 280 | if($this->runtimeConfig()->getPreQuery()){ 281 | //说明已经执行了预查询 282 | if(isset($this->__preQueryData[$targetModelClass])){ 283 | $whereVal = $this->getAttr($currentModeWhereCol); 284 | $data = $this->__preQueryData[$targetModelClass][$whereVal]; 285 | $target->data($data); 286 | return $target; 287 | } 288 | //没有执行过,则构建执行 289 | //构建ids 290 | $ids = []; 291 | foreach ($this->lastQueryResult()->getResult() as $item){ 292 | $ids[] = $item[$currentModeWhereCol]; 293 | } 294 | 295 | // TODO 还没有处理别名 296 | $builder->join($this->tableName(),"{$target->tableName()}.{$targetModeWhereCol} = {$this->tableName()}.{$currentModeWhereCol}",$joinType); 297 | $builder->where("{$this->tableName()}.{$currentModeWhereCol}",$ids,"IN"); 298 | $builder->limit($this->runtimeConfig()->getPreQuery()[1])->get($target->tableName()); 299 | //返回以父类定义的where key的结果集 300 | $list = []; 301 | $data = $this->__exec($builder,false); 302 | foreach ($data as $item){ 303 | $list[$item[$targetModeWhereCol]] = $item; 304 | } 305 | return [ 306 | "model"=>$targetModelClass, 307 | "dataList"=>$list, 308 | 'parentCol'=>$currentModeWhereCol 309 | ]; 310 | }else{ 311 | $whereVal = $this->getAttr($currentModeWhereCol); 312 | $builder->where($targetModeWhereCol,$whereVal)->limit(2)->get($target->tableName()); 313 | $data = $this->__exec($builder,false); 314 | if(!empty($data)){ 315 | //如果存在两条记录,说明关联关系或者是数据库存储有问题 316 | if(count($data) > 1){ 317 | throw new ModelError(static::class." mapOne() to {$targetModelClass} relation error,more than one record match"); 318 | }else{ 319 | $target->data($data[0]); 320 | return $target; 321 | } 322 | } 323 | } 324 | return null; 325 | } 326 | 327 | public function all(?array $ids = null,?string $idsCol = null):array 328 | { 329 | $builder = $this->__makeBuilder(); 330 | if($ids != null){ 331 | if($idsCol == null){ 332 | $idsCol = $this->__tablePk(); 333 | } 334 | $builder->where($idsCol,$ids,"in"); 335 | } 336 | 337 | $builder->get($this->tableName()); 338 | 339 | $data = $this->__exec($builder); 340 | 341 | $rawWithResult = []; 342 | if($this->runtimeConfig()->getPreQuery()){ 343 | //清空上次的执行结果 344 | $info = $this->runtimeConfig()->getPreQuery(); 345 | $withCols = $info[0]; 346 | foreach ($withCols as $col){ 347 | $rawWithResult[] = call_user_func([$this,$col]); 348 | } 349 | } 350 | 351 | $list = []; 352 | 353 | foreach ($data as $item){ 354 | $temp = new static(); 355 | $temp->data($item); 356 | $tempArr = []; 357 | foreach ($rawWithResult as $withColResult){ 358 | $keyVal = $this->__data[$withColResult['parentCol']]; 359 | $tempArr[$withColResult['model']] = $withColResult['dataList'][$keyVal]; 360 | } 361 | $temp->runtimeConfig()->setPreQueryData($tempArr); 362 | $list[] = $temp; 363 | } 364 | 365 | $this->resetStatusRuntimeStatus(); 366 | 367 | return $list; 368 | } 369 | 370 | public function save():bool 371 | { 372 | $this->resetStatusRuntimeStatus(); 373 | } 374 | 375 | public function update(array $data = [],bool $saveMode = true) 376 | { 377 | //$saveMode 是否允许无条件update 378 | $this->resetStatusRuntimeStatus(); 379 | } 380 | 381 | public function preQuery(array $col,?int $limit = 65535):AbstractModel 382 | { 383 | $this->runtimeConfig()->setPreQuery([ 384 | $col,$limit 385 | ]); 386 | return $this; 387 | } 388 | 389 | public function jsonSerialize() 390 | { 391 | 392 | } 393 | 394 | public function toArray(?array $callMethods = null):array 395 | { 396 | $data = $this->__tableArray(); 397 | foreach ($data as $key => $val){ 398 | $val = $this->getAttr($key); 399 | $data[$key] = $val; 400 | } 401 | if($callMethods == null){ 402 | $callMethods = []; 403 | } 404 | foreach ($callMethods as $callMethod){ 405 | if(method_exists($this,$callMethod)){ 406 | $res = call_user_func([$this,$callMethod]); 407 | if(is_array($res)){ 408 | $data = $data + $res; 409 | }elseif($res instanceof AbstractModel){ 410 | $data = $data + $res->toArray($callMethods); 411 | } 412 | } 413 | } 414 | 415 | return $data; 416 | } 417 | 418 | function toRawArray():array 419 | { 420 | return $this->__data; 421 | } 422 | 423 | 424 | private function resetStatusRuntimeStatus() 425 | { 426 | $this->runtimeConfig()->reset(); 427 | } 428 | 429 | 430 | private function __tableArray():array 431 | { 432 | $key = "tableArray".$this->__modelhash(); 433 | $ret = RuntimeCache::getInstance()->get($key); 434 | if(is_array($ret)){ 435 | return $ret; 436 | } 437 | $table = $this->schemaInfo(); 438 | $data = $table->getColumns(); 439 | $list = []; 440 | /** @var Column $col */ 441 | foreach ($data as $col){ 442 | $list[$col->getColumnName()] = $col->getDefaultValue(); 443 | } 444 | RuntimeCache::getInstance()->set($key,$list); 445 | return $list; 446 | } 447 | 448 | function __tablePk():?string 449 | { 450 | $key = "tablePk".$this->__modelhash(); 451 | $ret = RuntimeCache::getInstance()->get($key); 452 | if($ret){ 453 | return $ret; 454 | } 455 | 456 | $keyCol = null; 457 | $table = $this->schemaInfo(); 458 | /** @var Column $column */ 459 | foreach ($table->getColumns() as $column){ 460 | if($column->getIsPrimaryKey()){ 461 | $keyCol = $column->getColumnName(); 462 | RuntimeCache::getInstance()->set($key,$keyCol); 463 | break; 464 | } 465 | } 466 | if($keyCol == null){ 467 | throw new ExecuteFail("table: {$this->tableName()} have no primary key"); 468 | } 469 | 470 | return $keyCol; 471 | } 472 | 473 | private function __modelHash():string 474 | { 475 | $key = md5(static::class.$this->tableName().$this->runtimeConfig()->getConnectionConfig()->getName()); 476 | return substr($key,8,16); 477 | } 478 | 479 | private function __exec(QueryBuilder $builder,bool $resetQueryResult = true) 480 | { 481 | $client = $this->runtimeConfig()->getClient(); 482 | 483 | if($resetQueryResult){ 484 | $this->__lastQueryResult = DbManager::getInstance() 485 | ->__exec($client,$builder,false,$this->runtimeConfig()->getConnectionConfig()->getTimeout()); 486 | 487 | return $this->__lastQueryResult->getResult(); 488 | }else{ 489 | return DbManager::getInstance() 490 | ->__exec($client,$builder,false,$this->runtimeConfig()->getConnectionConfig()->getTimeout())->getResult(); 491 | } 492 | } 493 | 494 | private function __makeBuilder():QueryBuilder 495 | { 496 | //构建query builder 497 | $builder = new QueryBuilder(); 498 | if($this->runtimeConfig()->getWithTotalCount()){ 499 | $builder->withTotalCount(); 500 | } 501 | foreach ($this->runtimeConfig()->getOrder() as $order){ 502 | $builder->orderBy(...$order); 503 | } 504 | foreach ($this->runtimeConfig()->getWhere() as $where){ 505 | $builder->where(...$where); 506 | } 507 | 508 | foreach ($this->runtimeConfig()->getGroupBy() as $group){ 509 | $builder->groupBy($group); 510 | } 511 | foreach ($this->runtimeConfig()->getJoin() as $join){ 512 | $builder->join(...$join); 513 | } 514 | if($this->runtimeConfig()->getLimit()){ 515 | $builder->limit($this->runtimeConfig()->getLimit()); 516 | } 517 | return $builder; 518 | } 519 | } -------------------------------------------------------------------------------- /src/ConnectionConfig.php: -------------------------------------------------------------------------------- 1 | name; 31 | } 32 | 33 | /** 34 | * @param string|null $name 35 | */ 36 | public function setName(?string $name): void 37 | { 38 | $this->name = $name; 39 | } 40 | 41 | /** 42 | * @return mixed 43 | */ 44 | public function getHost() 45 | { 46 | return $this->host; 47 | } 48 | 49 | /** 50 | * @param mixed $host 51 | */ 52 | public function setHost($host): void 53 | { 54 | $this->host = $host; 55 | } 56 | 57 | /** 58 | * @return mixed 59 | */ 60 | public function getUser() 61 | { 62 | return $this->user; 63 | } 64 | 65 | /** 66 | * @param mixed $user 67 | */ 68 | public function setUser($user): void 69 | { 70 | $this->user = $user; 71 | } 72 | 73 | /** 74 | * @return mixed 75 | */ 76 | public function getPassword() 77 | { 78 | return $this->password; 79 | } 80 | 81 | /** 82 | * @param mixed $password 83 | */ 84 | public function setPassword($password): void 85 | { 86 | $this->password = $password; 87 | } 88 | 89 | /** 90 | * @return mixed 91 | */ 92 | public function getDatabase() 93 | { 94 | return $this->database; 95 | } 96 | 97 | /** 98 | * @param mixed $database 99 | */ 100 | public function setDatabase($database): void 101 | { 102 | $this->database = $database; 103 | } 104 | 105 | /** 106 | * @return int 107 | */ 108 | public function getPort(): int 109 | { 110 | return $this->port; 111 | } 112 | 113 | /** 114 | * @param int $port 115 | */ 116 | public function setPort(int $port): void 117 | { 118 | $this->port = $port; 119 | } 120 | 121 | /** 122 | * @return int 123 | */ 124 | public function getTimeout(): int 125 | { 126 | return $this->timeout; 127 | } 128 | 129 | /** 130 | * @param int $timeout 131 | */ 132 | public function setTimeout(int $timeout): void 133 | { 134 | $this->timeout = $timeout; 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | public function getCharset(): string 141 | { 142 | return $this->charset; 143 | } 144 | 145 | /** 146 | * @param string $charset 147 | */ 148 | public function setCharset(string $charset): void 149 | { 150 | $this->charset = $charset; 151 | } 152 | 153 | /** 154 | * @return int 155 | */ 156 | public function getAutoPing(): int 157 | { 158 | return $this->autoPing; 159 | } 160 | 161 | /** 162 | * @param int $autoPing 163 | */ 164 | public function setAutoPing(int $autoPing): void 165 | { 166 | $this->autoPing = $autoPing; 167 | } 168 | 169 | 170 | 171 | } -------------------------------------------------------------------------------- /src/Db/MysqlClient.php: -------------------------------------------------------------------------------- 1 | debugTrace; 26 | } 27 | 28 | function setConnectionConfig(ConnectionConfig $config):MysqlClient 29 | { 30 | $this->connectionConfig = $config; 31 | return $this; 32 | } 33 | 34 | function getConnectionConfig():ConnectionConfig 35 | { 36 | if($this->connectionConfig == null){ 37 | $this->connectionConfig = DbManager::getInstance()->connectionConfig(); 38 | } 39 | return $this->connectionConfig; 40 | } 41 | 42 | function gc() 43 | { 44 | $this->secureCheck(); 45 | if($this->connected){ 46 | $this->close(); 47 | } 48 | } 49 | 50 | function objectRestore() 51 | { 52 | $this->secureCheck(); 53 | /** 54 | * 连接对象归还的时候,如果遇到存在事务, 55 | * 说明编程的时候漏了提交或者回滚,为避免生产脏数据, 56 | * 强制回滚,回滚失败则断开连接,释放事务 57 | */ 58 | if($this->isInTransaction || $this->hasLock){ 59 | $this->close(); 60 | $this->connected = false; 61 | } 62 | } 63 | 64 | function beforeUse(): ?bool 65 | { 66 | return $this->connected; 67 | } 68 | 69 | /** 70 | * @param $timeout 71 | * @return bool 72 | * 重写父类方法,实现事务标记 73 | */ 74 | function begin($timeout = null):bool 75 | { 76 | throw new ExecuteFail("transaction are forbid call for mysql client"); 77 | } 78 | 79 | function commit($timeout = null):bool 80 | { 81 | throw new ExecuteFail("transaction are forbid call for mysql client"); 82 | } 83 | 84 | function rollback($timeout = null) 85 | { 86 | throw new ExecuteFail("transaction are forbid call for mysql client"); 87 | } 88 | 89 | function execQueryBuilder(QueryBuilder $builder, bool $raw = false, float $timeout = null):QueryResult 90 | { 91 | $this->debugTrace[] = clone $builder; 92 | 93 | if($timeout == null){ 94 | $this->getConnectionConfig()->getTimeout(); 95 | } 96 | 97 | $this->errno = 0; 98 | $this->error = ''; 99 | $this->insert_id = 0; 100 | $this->affected_rows = 0; 101 | 102 | $result = new QueryResult(); 103 | 104 | if($raw){ 105 | $ret = $this->query($builder->getLastQuery(),$timeout); 106 | }else{ 107 | $stmt = $this->prepare($builder->getLastPrepareQuery()); 108 | if($stmt){ 109 | $ret = $stmt->execute($builder->getLastBindParams()); 110 | if($ret === false && $this->errno){ 111 | $e = new ExecuteFail($this->error); 112 | $e->setQueryBuilder($builder); 113 | throw $e; 114 | } 115 | }else{ 116 | $e = new PrepareFail($this->error); 117 | $e->setQueryBuilder($builder); 118 | throw $e; 119 | } 120 | } 121 | $result->setResult($ret); 122 | $result->setLastError($this->error); 123 | $result->setLastErrorNo($this->errno); 124 | $result->setLastInsertId($this->insert_id); 125 | $result->setAffectedRows($this->affected_rows); 126 | 127 | $op = $builder->getLastTransactionOp(); 128 | 129 | switch ($op){ 130 | case QueryBuilder::TS_OP_START:{ 131 | if($ret == true){ 132 | $this->isInTransaction = true; 133 | } 134 | break; 135 | } 136 | case QueryBuilder::TS_OP_ROLLBACK: 137 | case QueryBuilder::TS_OP_COMMIT:{ 138 | if($ret){ 139 | $this->isInTransaction = false; 140 | } 141 | break; 142 | } 143 | 144 | case QueryBuilder::TS_OP_LOCK_TABLE:{ 145 | if($ret){ 146 | $this->hasLock = true; 147 | } 148 | break; 149 | } 150 | case QueryBuilder::TS_OP_UNLOCK_TABLE:{ 151 | if($ret){ 152 | $this->hasLock = false; 153 | } 154 | break; 155 | } 156 | 157 | case QueryBuilder::TS_OP_LOCK_IN_SHARE: 158 | case QueryBuilder::TS_OP_LOCK_FOR_UPDATE:{ 159 | //执行后自动释放,不需要标记。 160 | break; 161 | } 162 | } 163 | 164 | return $result; 165 | } 166 | 167 | private function secureCheck() 168 | { 169 | //如果处于事务中 170 | try{ 171 | if($this->isInTransaction || $this->hasLock){ 172 | $call = DbManager::getInstance()->onSecureEvent(); 173 | call_user_func($call,$this->debugTrace,$this->getConnectionConfig()); 174 | } 175 | }catch (\Throwable $exception){ 176 | $this->debugTrace = []; 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /src/Db/Pool.php: -------------------------------------------------------------------------------- 1 | getConfig()->toArray()); 17 | $client = new MysqlClient(); 18 | if($client->connect($mysqlConfig->toArray())){ 19 | $client->__lastPingTime = 0; 20 | $client->setConnectionConfig(new ConnectionConfig($this->getConfig()->toArray())); 21 | return $client; 22 | }else{ 23 | throw new Exception("mysql client connect to {$mysqlConfig->getHost()}:{$mysqlConfig->getPort()} error: {$client->connect_error}"); 24 | } 25 | } 26 | 27 | protected function itemIntervalCheck($item): bool 28 | { 29 | /** @var ConnectionConfig $config */ 30 | $config = $this->getConfig(); 31 | 32 | /** 33 | * auto ping是为了保证在 idleMaxTime周期内的可用性 (如果超出了周期还没使用,则代表现在进程空闲,可以先回收) 34 | */ 35 | if($config->getAutoPing() > 0 && (time() - $item->__lastPingTime > $config->getAutoPing())){ 36 | try{ 37 | //执行一个sql触发活跃信息 38 | $item->rawQuery('select 1'); 39 | // 标记最后一次ping的时间 不修改__lastUseTime是为了让idleCheck 在空闲的时候正常回收 40 | $item->__lastPingTime = time(); 41 | return true; 42 | }catch (\Throwable $throwable){ 43 | //异常说明该链接出错了,return 进行回收 44 | return false; 45 | } 46 | }else{ 47 | return true; 48 | } 49 | } 50 | 51 | /** 52 | * @param int|null $num 53 | * @return int 54 | * 屏蔽在定时周期检查的时候,出现连接创建出错,导致进程退出。 55 | */ 56 | public function keepMin(?int $num = null): int 57 | { 58 | try{ 59 | return parent::keepMin($num); 60 | }catch (\Throwable $throwable){ 61 | return $this->status(true)['created']; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Db/QueryResult.php: -------------------------------------------------------------------------------- 1 | lastInsertId; 20 | } 21 | 22 | /** 23 | * @param mixed $lastInsertId 24 | */ 25 | public function setLastInsertId($lastInsertId): void 26 | { 27 | $this->lastInsertId = $lastInsertId; 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getResult() 34 | { 35 | return $this->result; 36 | } 37 | 38 | /** 39 | * @param mixed $result 40 | */ 41 | public function setResult($result): void 42 | { 43 | $this->result = $result; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getLastError() 50 | { 51 | return $this->lastError; 52 | } 53 | 54 | /** 55 | * @param mixed $lastError 56 | */ 57 | public function setLastError($lastError): void 58 | { 59 | $this->lastError = $lastError; 60 | } 61 | 62 | /** 63 | * @return mixed 64 | */ 65 | public function getLastErrorNo() 66 | { 67 | return $this->lastErrorNo; 68 | } 69 | 70 | /** 71 | * @param mixed $lastErrorNo 72 | */ 73 | public function setLastErrorNo($lastErrorNo): void 74 | { 75 | $this->lastErrorNo = $lastErrorNo; 76 | } 77 | 78 | /** 79 | * @return mixed 80 | */ 81 | public function getAffectedRows() 82 | { 83 | return $this->affectedRows; 84 | } 85 | 86 | /** 87 | * @param mixed $affectedRows 88 | */ 89 | public function setAffectedRows($affectedRows): void 90 | { 91 | $this->affectedRows = $affectedRows; 92 | } 93 | 94 | /** 95 | * @return int 96 | */ 97 | public function getTotalCount(): int 98 | { 99 | return $this->totalCount; 100 | } 101 | 102 | /** 103 | * @param int $totalCount 104 | */ 105 | public function setTotalCount(int $totalCount): void 106 | { 107 | $this->totalCount = $totalCount; 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/DbManager.php: -------------------------------------------------------------------------------- 1 | onSecureEvent = function (array $traces,ConnectionConfig $config){ 32 | echo "connectionName [{$config->getName()}] for {$config->getHost()}:{$config->getPort()}@{$config->getUser()} may has un commit transaction or un release table lock:\n"; 33 | /** @var QueryBuilder $trace */ 34 | foreach ($traces as $trace){ 35 | echo "\t".$trace->getLastQuery()."\n"; 36 | } 37 | }; 38 | } 39 | 40 | function onSecureEvent(?callable $call = null):?callable 41 | { 42 | if($call != null){ 43 | $this->onSecureEvent = $call; 44 | } 45 | return $this->onSecureEvent; 46 | } 47 | 48 | function addConnection(ConnectionConfig $config):DbManager 49 | { 50 | $this->config[$config->getName()] = $config; 51 | return $this; 52 | } 53 | 54 | function connectionConfig(string $connectionName = "default"):ConnectionConfig 55 | { 56 | if(isset($this->config[$connectionName])){ 57 | return $this->config[$connectionName]; 58 | }else{ 59 | throw new PoolError("connection: {$connectionName} did not register yet"); 60 | } 61 | } 62 | 63 | function setOnQuery(?callable $func = null):?callable 64 | { 65 | if($func){ 66 | $this->onQuery = $func; 67 | } 68 | return $this->onQuery; 69 | } 70 | 71 | function fastQuery(?string $connectionName = "default"):QueryExecutor 72 | { 73 | if(isset($this->config[$connectionName])){ 74 | return (new QueryExecutor())->setConnectionConfig($this->config[$connectionName]); 75 | }else{ 76 | throw new PoolError("connection: {$connectionName} did not register yet"); 77 | } 78 | } 79 | 80 | function invoke(callable $call,string $connectionName = "default",float $timeout = null) 81 | { 82 | if($timeout == null){ 83 | $this->config[$connectionName]->getTimeout(); 84 | } 85 | $obj = $this->getConnectionPool($connectionName)->getObj($timeout); 86 | if($obj){ 87 | try{ 88 | return call_user_func($call,$obj); 89 | }catch (\Throwable $exception){ 90 | throw $exception; 91 | }finally { 92 | $this->getConnectionPool($connectionName)->recycleObj($obj); 93 | } 94 | }else{ 95 | throw new PoolError("connection: {$connectionName} getObj() timeout,pool may be empty"); 96 | } 97 | } 98 | 99 | function defer(string $connectionName = "default",?float $timeout = null):MysqlClient 100 | { 101 | $obj = $this->getConnectionPool($connectionName)->defer($timeout); 102 | if($obj){ 103 | return $obj; 104 | }else{ 105 | throw new PoolError("connection: {$connectionName} defer() timeout,pool may be empty"); 106 | } 107 | } 108 | 109 | function __exec(MysqlClient $client,QueryBuilder $builder,bool $raw = false,?float $timeout = null):QueryResult 110 | { 111 | $start = microtime(true); 112 | $result = $client->execQueryBuilder($builder,$raw,$timeout); 113 | if($this->onQuery){ 114 | $temp = clone $builder; 115 | call_user_func($this->onQuery,$result,$temp,$start,$client); 116 | } 117 | if(in_array('SQL_CALC_FOUND_ROWS',$builder->getLastQueryOptions())){ 118 | $temp = new QueryBuilder(); 119 | $temp->raw('SELECT FOUND_ROWS() as count'); 120 | $count = $client->execQueryBuilder($temp,false,$timeout); 121 | if($this->onQuery){ 122 | call_user_func($this->onQuery,$count,$temp,$start,$client); 123 | } 124 | $result->setTotalCount($count->getResult()[0]['count']); 125 | } 126 | return $result; 127 | } 128 | 129 | 130 | public function startTransaction(?MysqlClient $client = null):bool 131 | { 132 | $query = new QueryBuilder(); 133 | $query->raw('start transaction'); 134 | if($client == null){ 135 | $client = $this->defer(); 136 | } 137 | return $this->__exec($client,$query,true)->getResult(); 138 | } 139 | 140 | public function commit(?MysqlClient $client = null):bool 141 | { 142 | $query = new QueryBuilder(); 143 | $query->raw('commit'); 144 | if($client == null){ 145 | $client = $this->defer(); 146 | } 147 | return $this->__exec($client,$query,true)->getResult(); 148 | } 149 | 150 | public function rollback(?MysqlClient $client = null):bool 151 | { 152 | $query = new QueryBuilder(); 153 | $query->raw("rollback"); 154 | if($client == null){ 155 | $client = $this->defer(); 156 | } 157 | return $this->__exec($client,$query,true)->getResult(); 158 | } 159 | 160 | 161 | 162 | function resetPool(bool $clearTimer = true) 163 | { 164 | /** 165 | * @var $key 166 | * @var AbstractPool $pool 167 | */ 168 | foreach ($this->pool as $key => $pool){ 169 | $pool->reset(); 170 | } 171 | $this->pool = []; 172 | if($clearTimer){ 173 | Timer::clearAll(); 174 | } 175 | } 176 | 177 | function runInMainProcess(callable $func,bool $clearTimer = true) 178 | { 179 | $scheduler = new Scheduler(); 180 | $scheduler->add(function ()use($func,$clearTimer){ 181 | $func($this); 182 | $this->resetPool($clearTimer); 183 | }); 184 | $scheduler->start(); 185 | 186 | } 187 | 188 | private function getConnectionPool(string $connectionName):Pool 189 | { 190 | if(isset($this->pool[$connectionName])){ 191 | return $this->pool[$connectionName]; 192 | } 193 | $conf = $this->connectionConfig($connectionName); 194 | $pool = new Pool($conf); 195 | $this->pool[$connectionName] = $pool; 196 | return $pool; 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | queryBuilder; 21 | } 22 | 23 | /** 24 | * @param QueryBuilder|null $queryBuilder 25 | */ 26 | public function setQueryBuilder(?QueryBuilder $queryBuilder): void 27 | { 28 | $this->queryBuilder = $queryBuilder; 29 | } 30 | 31 | /** 32 | * @return string|null 33 | */ 34 | public function getSql(): ?string 35 | { 36 | return $this->sql; 37 | } 38 | 39 | /** 40 | * @param string|null $sql 41 | */ 42 | public function setSql(?string $sql): void 43 | { 44 | $this->sql = $sql; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Exception/ExecuteFail.php: -------------------------------------------------------------------------------- 1 | connectionConfig = $config; 23 | return $this; 24 | } 25 | 26 | /** @var QueryResult|null */ 27 | private $lastQueryResult; 28 | 29 | function lastQueryResult():?QueryResult 30 | { 31 | return $this->lastQueryResult; 32 | } 33 | 34 | function setClient(MysqlClient $client):QueryExecutor 35 | { 36 | $this->client = $client; 37 | return $this; 38 | } 39 | 40 | function get($tableName, $numRows = null, $columns = null) 41 | { 42 | parent::get($tableName, $numRows, $columns); 43 | return $this->exec(); 44 | } 45 | 46 | function update($tableName, $tableData, $numRows = null) 47 | { 48 | parent::update($tableName, $tableData, $numRows); 49 | return $this->exec(); 50 | } 51 | 52 | function delete($tableName, $numRows = null) 53 | { 54 | parent::delete($tableName, $numRows); 55 | return $this->exec(); 56 | } 57 | 58 | function getOne($tableName, $columns = '*') 59 | { 60 | parent::getOne($tableName, $columns); 61 | $ret = $this->exec(); 62 | if($ret){ 63 | return $ret[0]; 64 | } 65 | return null; 66 | } 67 | 68 | function insert($tableName, $insertData) 69 | { 70 | parent::insert($tableName, $insertData); 71 | return $this->exec(); 72 | } 73 | 74 | function insertAll($tableName, $insertData, $option = []) 75 | { 76 | parent::insertAll($tableName, $insertData, $option); 77 | return $this->exec(); 78 | } 79 | 80 | function execRaw($sql, $param = []) 81 | { 82 | parent::raw($sql, $param); 83 | return $this->exec(true); 84 | } 85 | 86 | public function startTransaction() 87 | { 88 | parent::startTransaction(); 89 | return $this->exec(true); 90 | } 91 | 92 | public function commit() 93 | { 94 | parent::commit(); 95 | return $this->exec(true); 96 | } 97 | 98 | public function rollback() 99 | { 100 | parent::rollback(); 101 | return $this->exec(true); 102 | } 103 | 104 | public function lockTable($table) 105 | { 106 | parent::lockTable($table); 107 | return $this->exec(true); 108 | } 109 | 110 | public function unlockTable() 111 | { 112 | parent::unlockTable(); 113 | return $this->exec(true); 114 | } 115 | 116 | private function getClient():MysqlClient 117 | { 118 | if($this->client){ 119 | return $this->client; 120 | }else{ 121 | return DbManager::getInstance()->defer($this->connectionConfig->getName()); 122 | } 123 | } 124 | 125 | private function exec(bool $raw = false) 126 | { 127 | $this->lastQueryResult = DbManager::getInstance()->__exec($this->getClient(),$this,$raw); 128 | return $this->lastQueryResult->getResult(); 129 | } 130 | } -------------------------------------------------------------------------------- /src/RuntimeCache.php: -------------------------------------------------------------------------------- 1 | data[$key] = $val; 16 | } 17 | 18 | function get($key) 19 | { 20 | if(isset($this->data[$key])){ 21 | return $this->data[$key]; 22 | } 23 | return null; 24 | } 25 | 26 | function clear() 27 | { 28 | $this->data = []; 29 | } 30 | } -------------------------------------------------------------------------------- /src/RuntimeConfig.php: -------------------------------------------------------------------------------- 1 | connectionConfig = $config; 35 | return $this; 36 | } 37 | 38 | function getConnectionConfig():ConnectionConfig 39 | { 40 | if($this->connectionConfig == null){ 41 | $this->connectionConfig = DbManager::getInstance()->connectionConfig(); 42 | } 43 | return $this->connectionConfig; 44 | } 45 | 46 | function getClient():MysqlClient 47 | { 48 | if($this->client){ 49 | return $this->client; 50 | }else{ 51 | return DbManager::getInstance()->defer($this->getConnectionConfig()->getName()); 52 | } 53 | } 54 | 55 | function setClient(MysqlClient $client):RuntimeConfig 56 | { 57 | $this->client = $client; 58 | return $this; 59 | } 60 | 61 | public function where(...$args) 62 | { 63 | $this->where = $args; 64 | } 65 | 66 | public function getWhere():array 67 | { 68 | return $this->where; 69 | } 70 | 71 | public function order(...$args) 72 | { 73 | $this->order[] = $args; 74 | return $this; 75 | } 76 | 77 | public function limit(int $one, ?int $two = null) 78 | { 79 | if ($two !== null) { 80 | $this->limit = [$one, $two]; 81 | } else { 82 | $this->limit = $one; 83 | } 84 | return $this; 85 | } 86 | 87 | public function getLimit() 88 | { 89 | return $this->limit; 90 | } 91 | 92 | public function field($fields) 93 | { 94 | if (!is_array($fields)) { 95 | $fields = [$fields]; 96 | } 97 | $this->fields = $fields; 98 | return $this; 99 | } 100 | 101 | public function withTotalCount() 102 | { 103 | $this->withTotalCount = true; 104 | return $this; 105 | } 106 | 107 | public function getWithTotalCount():bool 108 | { 109 | return $this->withTotalCount; 110 | } 111 | 112 | public function getOrder():array 113 | { 114 | return $this->order; 115 | } 116 | 117 | public function getGroupBy():array 118 | { 119 | return $this->groupBy; 120 | } 121 | 122 | public function groupBy($filed) 123 | { 124 | $this->groupBy[] = $filed; 125 | return $this; 126 | } 127 | 128 | public function join(...$args) 129 | { 130 | $this->join[] = $args; 131 | return $this; 132 | } 133 | 134 | public function getJoin():array 135 | { 136 | return $this->join; 137 | } 138 | 139 | /** 140 | * @return array|null 141 | */ 142 | public function getPreQuery(): ?array 143 | { 144 | return $this->preQuery; 145 | } 146 | 147 | /** 148 | * @param array|null $preQuery 149 | */ 150 | public function setPreQuery(?array $preQuery): void 151 | { 152 | $this->preQuery = $preQuery; 153 | } 154 | 155 | /** 156 | * @return array|null 157 | */ 158 | public function getPreQueryData(): ?array 159 | { 160 | return $this->preQueryData; 161 | } 162 | 163 | /** 164 | * @param array|null $preQueryData 165 | */ 166 | public function setPreQueryData(?array $preQueryData): void 167 | { 168 | $this->preQueryData = $preQueryData; 169 | } 170 | 171 | 172 | public function reset() 173 | { 174 | $this->fields = "*"; 175 | $this->limit = null; 176 | $this->withTotalCount = false; 177 | $this->order = []; 178 | $this->where = []; 179 | $this->join = []; 180 | $this->groupBy = []; 181 | $this->preQuery = null; 182 | $this->preQueryData = null; 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | # 注册一个连接 3 | ```php 4 | use EasySwoole\ORM\ConnectionConfig; 5 | use EasySwoole\ORM\DbManager; 6 | 7 | $config = [ 8 | 'host' => '', 9 | 'port' => , 10 | 'user' => '', 11 | 'password' => '', 12 | 'database' => '' 13 | ]; 14 | $con = new ConnectionConfig($config); 15 | DbManager::getInstance()->addConnection($con); 16 | 17 | ``` 18 | 19 | # 主进程无协程环境使用(CLI) 20 | ```php 21 | use EasySwoole\ORM\DbManager; 22 | 23 | DbManager::getInstance()->runInMainProcess(function (DbManager $dbManager){ 24 | $ret = $dbManager->fastQuery()->raw("select version()"); 25 | var_dump($ret); 26 | }); 27 | ``` --------------------------------------------------------------------------------