{"id":27118451,"url":"https://github.com/suconghou/mvc","last_synced_at":"2025-06-16T07:08:47.322Z","repository":{"id":12549610,"uuid":"15219783","full_name":"suconghou/mvc","owner":"suconghou","description":"fast,light weight php framework","archived":false,"fork":false,"pushed_at":"2024-04-06T09:21:00.000Z","size":1247,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-04-06T10:25:15.194Z","etag":null,"topics":["framework","micro-framework","microservice","mvc","mysql","php","router","sqlite","validator"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/suconghou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2013-12-16T07:37:38.000Z","updated_at":"2024-04-06T10:25:16.130Z","dependencies_parsed_at":"2023-11-16T05:29:18.216Z","dependency_job_id":"7d7563ad-0bbf-4f57-bb15-9b20738de4e6","html_url":"https://github.com/suconghou/mvc","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/suconghou/mvc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suconghou%2Fmvc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suconghou%2Fmvc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suconghou%2Fmvc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suconghou%2Fmvc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/suconghou","download_url":"https://codeload.github.com/suconghou/mvc/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suconghou%2Fmvc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260116643,"owners_count":22961065,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","micro-framework","microservice","mvc","mysql","php","router","sqlite","validator"],"created_at":"2025-04-07T07:58:02.142Z","updated_at":"2025-06-16T07:08:47.290Z","avatar_url":"https://github.com/suconghou.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 简单易用的 MVC 开发框架\n\n---\n\n\n\n## 框架特色\n\u003e * 核心代码不足1000行,仅两个文件便可工作,极速加载\n\u003e * 单文件入口,不依赖`PathInfo`,入口文件即是配置文件,超级简洁\n\u003e * 文件夹随意移动,轻松多项目共享,入口文件随意命名,CLI模式轻松使用\n\u003e * `MYSQL/SQLITE`任意切换,注入过滤/ORM,文件缓存/HTTP缓存/数据库缓存,轻松安全\n\u003e * 异常捕获,`DEBUG`日志,自定义错误页,自定义异常路由一应俱全\n\u003e * 普通路由,正则路由,回调处理,百变URI随心所欲,插件模式,即插即用\n\u003e * 文件加载自动完成,延迟按需加载、无需`Include`简单高效\n\n---\n\n## 安装配置\n\n- `index.php`入口文件即配置文件,`core.php`框架核心,外加一个处理请求的控制器文件\n- `PHP8.0`及以上\n- 使用PDO连接数据库,支持`MySQL`和`Sqlite`,需开启`PDO_MYSQL`\n- 定义配置文件的程序路径(一般不需改变)和其他参数,例如SMTP,数据库,即可完美使用\n- 需要URL REWRITE支持,否则链接中要添加`index.php`\n- rewrite 即为一般的index.php rewrite写法\n\n对于`nginx`\n\n```nginx\ntry_files $uri $uri/ /index.php$is_args$args;\n```\n\n加入`location / {}`里面\n\n对于`apache`\n\n```\nRewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule ^(.*)$ index.php [QSA,L]\n```\n\nWeb程序可将目录结构调整为\n\n```\n├── app\n│  ├── controller\n│  │  └── home.php\n│  ├── model\n│  │  └── util.php\n│  └── system\n│     └── core.php\n├── README.md\n├── var\n│  ├── html\n│  └── log\n└── www\n   └── index.php\n```\n\n----\n\n## 开始使用\n\n---\n\n入口文件`index.php`即为配置文件,主要配置控制器模型等地址\n\n配置项`lib_path`为一个数组,配置控制器,模型,类库的自动查找地址\n\n`view_path`为模板查找文件夹\n\n`var_path`为缓存目录和日志目录\n\n文件夹 controller model system 其实并无区别\n\n系统并不是按照文件夹结构来区分 controller 和 model 而是按照类本身的特性\n\n包含`__invoke`魔术方法的类将被视为控制器\n\n在控制器抛出异常时,此方法将被调用用于处理异常.\n\n`event`字段配置闭包函数可用于扩展`app`\n\n\n- 所有加载过的控制器,都会记住是否已加载过,不会重复加载,也不会重复实例化\n- 加载过的类库和模型全局可用\n- 所有的同名文件类都可以直接通过类名调用其静态方法,或new实例化\n\n\n对于低于8.1版本，需要实现`array_is_list`函数\n```php\nif (!function_exists('array_is_list'))\n{\n    function array_is_list(array $a)\n    {\n        return $a === [] || (array_keys($a) === range(0, count($a) - 1));\n    }\n}\n```\n对于低于8.0版本，需要实现`str_contains`函数\n```php\nif (!function_exists('str_contains'))\n{\n\tfunction str_contains(string $haystack, string $needle)\n\t{\n\t\treturn empty($needle) || strpos($haystack, $needle) !== false;\n\t}\n}\n```\n\n\n## DEBUG \n\n配置文件`debug`字段用于控制日志记录,其值可为`0/1` 或者 `false/true`\n\n框架判断其布尔值,非`debug`模式仅记录`ERROR`级别日志，忽略`INFO`,`WARN`\n\n无论是否debug,只要日志目录可写，都会记录相应的错误。\n\n\n\n\n-----\n\n---\n\n## 路由\n\n框架同时包含正则路由和普通路由.\n\n普通路由按照目录结构路由,\n\n正则路由按照URI匹配.\n\n\n**_使用 route::get() 添加正则路由_**\n\n正则路由优先级高于普通路由.\n\n\n正则路由有多种写法\n\n字符串函数\n\n```php\nroute::get('/print','print_r');\n```\n\n闭包\n```php\nroute::get('/dump',function(...$a){\n\tvar_dump($a);\n});\n\n```\n\n实例化控制器\n\n控制器类将被自动实例化,然后执行\n\n```php\nroute::get('/hello',['home','hello'])\nroute::get('/hello',['home','hello','world'])\n```\n\n静态化控制器\n\n控制器类方法将被静态调用\n\n```php\n\nroute::post('/static','home::echo');\n\n```\n\n捕获参数\n```php\nroute::get('/userinfo/(\\d+)',['home','userinfo'])\n```\n\n\n带命名空间的静态调用,可调度到子文件夹,自己再次分发路由,`admin`为`namespace`,`form`类无实例化，直接调用\n```php\nroute::get('/hi/index','admin\\form::index')\n```\n\n\n带命名空间的动态调用,可调度到子文件夹,`form`类校验，并实例化\n```php\nroute::get('/hello/hi',['admin\\form','hi'])\n```\n二级，三级，文件夹同类\n\n\n闭包模式\n```php\nroute::get('/about',function(){echo 'about';})\n```\n\n\n**URL 拼接**\n```php\nroute::u('/home/hello',['act'=\u003e'hi'])\n```\n\n**重定向**\n```php\nroute::to('/login')\n```\n\n## 控制器\n\n默认的控制器为`home`,默认的action为`index`\n\n控制器被实例化时,将传入触发实例化时的路由参数给构造函数\n\n控制器实现了单例模式,一个控制器只会实例化一次\n\n`app::run(array $r)` 可以内部转移控制器,交由其他控制器执行\n\n## 自定义异常处理器\n\n除了控制器的`__invoke`方法能处理异常外,全局异常可配置自定义处理\n\n在配置中添加`notfound`和`errfound`并赋值一个闭包,开启自定义错误处理\n\n```php\n'notfound' =\u003e function ($e, $cli) {\n\techo '404 page not found';\n},\n'errfound' =\u003e function ($e, $cli) {\n\techo 'user hand this error : ', $e-\u003egetMessage();\n},\n\n```\n\n自定义异常处理后,框架提供的404,500等错误详情,页面将不再展现,只能通过日志查看\n\n\n\n\n## 使用缓存\n\n框架內建2种缓存,可同时工作\n\n### HTTP 缓存\n\nHTTP 缓存使用 `app::cache(int $second)`开启\n\n需要在控制器输出其他内容前调用\n\n客户端下次请求将会`200(from cache)`\n\n有客户端发起协商缓存时,框架也会自动验证,命中时返回`304`\n\n可用于缓存接口,页面等\n\n### 页面缓存\n\n模板函数`template(string $v, array $data = [], callable|int $callback = 0, string $path = '')`实现了页面缓存\n\n设置`$callback`为一个大于1的秒数,即可开启页面自动缓存\n\n同时必须确保配置项`var_path`是可写的,缓存文件存储在其`html`子文件夹\n\n页面缓存的缓存键是当前请求的`URI`,不包含query部分\n\n可用于缓存公共页面,不可缓存带有个人信息的页面\n\n请求来临时同样鉴别对应的`URI`是否已缓存\n\n命中时释放缓存,缓存失效时删除缓存\n\n配置`$callback`为1，则返回模板渲染的数据而不是直接输出，配置为闭包可以获取渲染结果，手动处理后续逻辑\n\n\n## 验证器\n\n`request::verify(array $rule, array|bool $post = true, bool|callable $callback = false)`\n\n`$post`为要验证的数据,若非数组，则true代表$_POST,false代表$_REQUEST\n\n`$rule`\n\n例:\n```php\n$r =\n\t[\n\t\t'q' =\u003e ['maxlength=50' =\u003e 'q最大50字符'],\n\t\t'type' =\u003e ['set=video' =\u003e 'type不合法'],\n\t\t'order' =\u003e ['set=viewCount' =\u003e 'order不合法'],\n\t\t'channelId' =\u003e ['/^[\\w\\-]{20,40}$/' =\u003e 'channelId为20-40位字母'],\n\t\t'pageToken' =\u003e ['/^[\\w\\-]{4,14}$/' =\u003e 'pageToken为4-14位字母'],\n\t\t'relatedToVideoId' =\u003e ['/^[\\w\\-]{4,14}$/' =\u003e 'relatedToVideoId为4-14位字母'],\n\t\t'maxResults' =\u003e ['number=1,50' =\u003e 'maxResults不合法'],\n\t\t'regionCode' =\u003e ['set=HK,TW,US,KR,JP' =\u003e 'regionCode不合法'],\n\t\t'part' =\u003e 'id,snippet',\n\t];\n\n```\n注意 `maxResults` 的配置项为一个数组,元素可为闭包和其他规则,\n如果直接写一个闭包而不是数组,代表使用闭包的返回值,而不是对输入值校验.\n\n\n內建的验证类型有\n\n类型限定\n```\n'array', 'bool', 'float', 'int', 'null', 'numeric', 'object', 'scalar', 'string'\n```\nctype类型\n```\n'alnum', 'alpha', 'cntrl', 'digit', 'graph', 'lower', 'print', 'punct', 'space', 'upper', 'xdigit'\n```\n其他 `require` `required` `default` `number` `json` `url` `ip` `email` `username` `password` `phone`\n\n动态比较的类型有 `minlength` `maxlength` `length` `eq` `eqs` `set` `int` `number` `numeric`\n\n\u003e required 数字0,字符串0,空数组,空字符串等被认为校验不通过,其他true值,被认为通过校验\n\n\u003e require 在required的基础上允许数字0和字符串0校验通过\n\u003e 注意:空数组,空字符串,被认为校验不通过, 与`required`的区别在于数字0和字符串0,`required`更加严格\n\n\u003e default 如果值不存在则使用此默认值并忽略规则校验,如果有值,则规则将会生效\n\n\u003e int,float等为强类型规则, number可以既接受int型也接受字符串格式的整数\n\n\u003e numeric可以既接受float型也接受字符串格式的小数,也可以既接受int型也接受字符串格式的整数\n\n\u003e int,number,numeric,length 可以使用区间限定,使用`,`连接两个区间,`int=1,50`表示int型1和50之间,`length=10,20`表示string类型长度在10至20\n\n\u003e 无`,`时,表示需大于等于此起始值,`number=1` 表示int或字符串整数，值大于等于1,即除0外的正整数\n\n\u003e `minlength=8` 表示string长度最小8\n\n\u003e set 规则只能针对值是string类型的值做判断,值为int类型的,一律判断不通过\n\n\u003e 直接使用数组语法来判断更加复杂的是否在集合中, 此比较方法是强类型的比较\n\n\u003e eqs为不区分大小写,eq为区分大小写\n\n\u003e 键可扩展为一种静态方法模式校验规则,例 r::domain 表示使用r类中的静态方法domain来校验，实现复杂校验\n\n定义`callback`值,用于检验不通过时的动作\n\n\u003e false 抛出异常,此为默认\n\n\u003e 闭包函数, 异常将传递给此闭包函数处理\n\n\u003e true 立即响应json并中断\n\n\n\n\n## 多数据库链接\n\n某个Model要链接其他数据库,只需要重写ready方法即可,然后返回新的PDO实例\n```php\npublic static function ready(): PDO\n{\n\tstatic $_pdo;\n\t$_pdo ??= self::init(app::get('dbdev'));\n\treturn $_pdo;\n}\n```\n\n\n## 轻量级的 ORM 操作\n\n无过度封装,简单直接,轻松完成大部分数据库操作.\n\n\nPDO参数\n```php\n$options = [PDO::ATTR_PERSISTENT =\u003e true, PDO::ATTR_ERRMODE =\u003e PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE =\u003e PDO::FETCH_ASSOC, PDO::ATTR_TIMEOUT =\u003e 3, PDO::ATTR_EMULATE_PREPARES =\u003e false, PDO::ATTR_STRINGIFY_FETCHES =\u003e false];\n```\n\n`ATTR_EMULATE_PREPARES`若是开启的话,数据库中的`int`和`float`取回来时,在PHP中被转化为`string`,造成类型不一致,故需要\n```\nATTR_EMULATE_PREPARES =\u003e false\nATTR_STRINGIFY_FETCHES =\u003e false\n```\n\n`ATTR_EMULATE_PREPARES`配置为`false`时,预编译中的同名命名参数(`:name`这样的形式)只能使用一次.\n见https://www.php.net/manual/en/pdo.prepare.php\n\n命名参数与`?`占位符也不可混用\n\n此框架都已自动处理.\n\n\n### 增加\n\n```php\ndb::insert(array $data,string $table='',bool $ignore=false,bool $replace=false)\ndb::replace(array $data,string $table='')\n```\n\n`replace`也是通过`insert`方法,只是参数不同.\n\n`$ignore`设置为`true`可以使用`INSERT IGNORE`模式\n\n\u003e _`insert`方法没有完成`ON DUPLICATE KEY UPDATE`,若想使用,见下面说明_\n\n\u003e _`insert`方法没有完成`INSERT DELAYED INTO`,若想使用,见下面说明_\n\n\u003e _对于批量插入,参见下面说明_\n\n### 删除\n\n```php\ndb::delete(array $where=[],string $table='')\n```\n\n将`$where`设置为空数组即可删除全表数据\n\n详细的`$where`使用见*WHERE 构造器*\n\n### 查询\n\n```php\ndb::find(array $where=[],string $table='',string $col='*',array $orderLimit=[],$fetch='fetchAll')\ndb::findOne(array $where=[],string $table='',string $col='*',array $orderLimit=[1],$fetch='fetch')\ndb::findVar(array $where=[],string $table='',string $col='COUNT(1)',array $orderLimit=[1])\ndb::findPage(array $where=[],string $table='',string $col='*',int $page=1,int $limit=20,array $order=[])\n```\n\n`findOne`,`findVar`,`findPage`均是借助于`find`方法,只不过传递参数不同.\n\n`findOne`默认`LIMIT 1`,只返回一行数据\n\n`findVar`在`findOne`的基础上仅返回一个字段,并且默认是`COUNT(1)`即计算行数,可以修改参数三返回希望的字段.\n\n`findPage`为分页,返回指定页的数据和上一页,下一页等,可以添加排序规则,详细见*ORDERLIMIT 构造器*\n\n详细的`$where`使用见*WHERE 构造器*\n\n#### 大量查询\n\n如果你的一条 SQL 要查询大量数据,结果集往往超过几十万条,一次读取结果集会使得内存溢出,脚本终止.\n\n其实`find`的第五个参数可以帮助你.\n\n该参数为获取结果集的方法,`find`方法默认是一次性全部获取为数组,你可以传入参数`true`交由自己主动获取.\n\n使用参数`true`后将返回一个`PDOStatement`,你将可以自由进行后续操作.\n\n```php\n$stm=User::find(['id \u003e'=\u003e1],'userTable','*',['id'=\u003e'ASC'],true);\nwhile($row=$stm-\u003efetch())\n{\n}\n```\n\n\u003e 注意: pdo-\u003eexec() 返回的是一个int值,代表被影响的行数, 例如runSql()函数\n\u003e\n\u003e stm-\u003eexecute() 返回的是一个布尔值,代表是否操作成功,\n\u003e 使用 stm-\u003erowCount() 才能取到执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。\n\n你可以修改方法`fetch`为`fetchObject`\n\n他们二者的不同是以数组还是对象的方式返回.\n\n即使循环获取,数据也是从 MySQL 服务器发送到了 PHP 进程中保存,若数据实在太大,可以设置数据任然保存在 MySQL 服务器,循环的过程中现场取\n\n在查询之前,给 PDO 实例设置\n\n```php\nself::setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,false);\n```\n\n然后再循环获取,内存使用会显著下降\n\n\u003e _因 PDO 使用长连接,该设置会影响一定时段内的所有 SQL 查询,你也可以查询完设置回`true`避免影响其他查询_ \n\u003e _自 PHP5.5 起,可以使用 yield,大数据量下可以显著帮你节省内存_\n\n#### 子查询\n\n使用原始值可以实现子查询\n\n```php\n$where=['!id IN'=\u003e'(SELECT `id` FROM `user` WHERE fid=1)','age \u003e'=\u003e18];\n```\n\n### 更新\n\n```php\ndb::update(array $where,array $data,string $table='')\n```\n\n`$where`的具体形式见*WHERE 构造器*\n\n`$data`的具体形式见*SET 构造器*\n\n### WHERE 构造器\n\n```php\ndb::condition(array \u0026$where,string $prefix='WHERE')\n```\n\n在查询和删除,更新等场景下,传入一个数组作为条件\n\n`$where`是一个数组变量,一般为一维数组,某些需要使用`IN`操作时为二维数组\n\n`$where`为一个引用,执行过后会清理`$where`中的数据,因此必须传入一个变量名,执行后的`$where`变量将有后续用途\n\n例如 `$where=['username'=\u003e'name1','age'=\u003e18];`\n\n这样会筛选`username`为`name1`并且`age`为 18 的用户\n\n\u003e 默认的结合条件是`AND`,你可以在数组的最后面添加一个指定的结合符\n\n\u003e 改用`OR`连接 `$where=['username'=\u003e'name1','age'=\u003e18,'OR'];`\n\n同时对于键(例如`username`)还可以添加一些修饰符和操作符.\n\n键可以由三部分组成,以空格隔开\n\n第一段对应于数据库中的字段,第二段是修饰符,往往是`NOT`或空,第三段是操作符,可以是`\u003e` , `\u003c` , `\u003e=` , `\u003c=` , `!=` , `\u003c\u003e` , `IN` , `LIKE` , `REGEXP`\n\n例如`$where=['age \u003e'=\u003e18,'name LIKE'=\u003e'user%'];`\n\n同理,也可以使用`NOT LIKE`\n\n如果你需要使用`IN`操作符,也是可以的,给它的键值为一个数组\n\n`$where=['id'=\u003e[1,3,5,7]];` 数组作为参数默认就置为`IN`操作符\n\n将会等到 `where id in (1,3,5,7)`,如果要使用`NOT IN`需要显式指明\n\n`$where=['id NOT IN'=\u003e[1,3,5,7]]`\n\n**_使用字段的引用和内置函数_**\n\n`$where`数组中的键值都会进行预处理操作,因此不能使用字段的引用和内置函数.\n\n若要使用,可以在键的第一段,即数据库字段前加`!`定义符,代表要使用原始值.\n\n`$where=['!time \u003c '=\u003e'UNIX_TIMESTAMP()']`\n\n使用`!`定义符后对应的键值须为定值,对于用户发送来的数据,使用`!`定义符前需要仔细过滤,仅能信任使用`intval`过滤后的值.\n\n`!time` 或者 `! time` `! time \u003c` `!time \u003c` 等都是合法的,\n\n但`!time\u003c` `! time\u003c`是非法的,字段和操作符之间必须使用空格隔开\n\n**NULL处理**\n\n当条件的值为`null`时,自动构造为`IS NULL`语句.\n\n例如 `['name'=\u003enull]` 转化为\n```sql\n`name` IS NULL\n```\n\n如果需要`IS NOT NULL`需要显示申明\n`['name IS NOT'=\u003enull]`\n\n此时,使用原始值前缀`!`对此构造无任何影响\n\n`['!name'=\u003enull]`\n\n`['!name IS NOT'=\u003enull]`\n\n都是合法的.\n\n**EXISTS** 与 **FIND_IN_SET**\n\n封装一个函数使用原始值模式对`where`规则添加额外的条件\n\n```php\nfinal public static function extra(array $cond, array $where = [], string $filed = null): array\n{\n\t$j = 'AND';\n\tforeach ($cond as $k =\u003e \u0026$v) {\n\t\tif (is_array($v)) {\n\t\t\t$v = sprintf('%s IN (%s)', (str_contains($k, '.') || str_contains($k, '`')) ? $k : \"`{$k}`\", implode(',', $v));\n\t\t} else if (is_null($v)) {\n\t\t\t$v = sprintf('%s IS NULL', (str_contains($k, '.') || str_contains($k, '`')) ? $k : \"`{$k}`\");\n\t\t} else if (is_string($k)) {\n\t\t\t$v = sprintf(\"FIND_IN_SET('%s',%s)\", trim($v, \" \\n\\r\\t\\v\\0\\\"'\"), (str_contains($k, '.') || str_contains($k, '`')) ? $k : \"`{$k}`\");\n\t\t\tif (ctype_alnum(str_replace('_', '', $k))) {\n\t\t\t\t$filed ??= $k;\n\t\t\t}\n\t\t} else if (is_bool($v) || is_int($v) || is_float($v)) {\n\t\t\t$j = $v ? 'AND' : 'OR';\n\t\t\tunset($cond[$k]);\n\t\t}\n\t}\n\tif ($cond) {\n\t\t$where[\"! $filed IS NOT NULL AND\"] = '(' . implode(\" $j \", $cond) . ')';\n\t}\n\treturn $where;\n}\n```\n\n\u003e 对`$cond`添加键值对，键代表字段，值代表此字段对应`FIND_IN_SET`需要包含的值\n\u003e\n\u003e 当没有键（键是数字），此时值为普通SQL语句，可以为`EXISTS`或其他子查询等\n\u003e\n\u003e 如果`$cond`没有合适的字段key，则需要指定`$filed`的值\n\n\n```php\nself::find(self::extra(['type' =\u003e 3], ['id \u003e' =\u003e 1]), 'messages')\n```\n构造出\n```sql\nSELECT * FROM `messages` WHERE (`id` \u003e :id_1 AND `type` IS NOT NULL AND (FIND_IN_SET('3',`type`)))\n```\n如果你需要`FIND_IN_SET`使用常量参数，或参数一为字段，需要用拼接模式\n```php\nself::find(self::extra([\"FIND_IN_SET(`id`,'1,2,3')\"], ['id \u003e' =\u003e 1], 'id'), 'messages');\n```\n此时FIND_IN_SET就相当于查询ID是IN(1,2,3),构造出\n```sql\nSELECT * FROM `messages` WHERE (`id` \u003e :id_1 AND `id` IS NOT NULL AND (FIND_IN_SET(`id`,'1,2,3')))\n```\n\n性能比较：当子查询的表比较大时，使用`EXISTS`可能比使用`IN`性能更好，\n`IN`首先完成内层查询，然后在外层查询的过程中一一过滤；`EXISTS`需要先完成外层查询，然后对所有记录一一执行`EXISTS`子句进行过滤\n\n```php\nself::find(self::extra([\"EXISTS(SELECT 1 FROM `contacts` WHERE contacts.`id`=messages.`cid` AND `name` \u003c\u003e 'group1' )\"], ['id \u003e' =\u003e 1], 'id'), 'messages');\nself::find(self::extra([\"messages.`cid` IN (SELECT `id` FROM `contacts` WHERE `name` \u003c\u003e 'group1')\"], ['id \u003e' =\u003e 1], 'id'), 'messages')\n```\n\n```sql\nSELECT * FROM `messages` WHERE (`id` \u003e :id_1 AND `id` IS NOT NULL AND (EXISTS(SELECT 1 FROM `contacts` WHERE contacts.`id`=messages.`cid` AND `name` \u003c\u003e 'group1' )))\nSELECT * FROM `messages` WHERE (`id` \u003e :id_1 AND `id` IS NOT NULL AND (messages.`cid` IN (SELECT `id` FROM `contacts` WHERE `name` \u003c\u003e 'group1')))\n```\n\n\u003e `NOT EXISTS`配合`INSERT INTO ... SELECT ...`还可以实现满足条件时插入\n\n\n**HAVING**\n\n简单的`HAVING`语句可以同上,修改condition后的参数,包含`GROUP BY`等复杂语句需要自己拼接\n\n```php\nfinal public static function findHaving(array $where = [], string $table = '', string $col = '*', array $orderLimit = [], $fetch = 'fetchAll')\n{\n\t$sql = sprintf('SELECT %s FROM %s%s%s', $col, static::table($table), self::condition($where, \"HAVING\"), $orderLimit ? self::orderLimit($orderLimit) : '');\n\treturn self::exec($sql, $where, $fetch);\n}\n```\n\n\n### SET 构造器\n\n```php\ndb::values(array \u0026$data,bool $set=false,string $table='')\n```\n\n`$data`使用关联数组表示,默认生成`VALUES()`语句用于`INSERT`,将`$set`设置为`true`生成用于`update`的语句\n\n`['name'=\u003e'name1','pass'=\u003e123]`\n\n数组的键也有一个前置定义符`!`,表示原始值,使用此定义符可以调用函数,引用字段等,插入原始值等.\n\n如 `['v'=\u003etime(),'!t'=\u003e'UNIX_TIMESTAMP()']` 添加了!则存储的是时间戳,不加!则是存储此字符串\n\n`['!count'=\u003e'count+1']` 使`count`的值加一\n\n`['!count'=\u003e'count+age']` 引用其他字段,`count`设置为`count+age`的和\n\n除非你要调用函数或引用字段,否则不建议你使用原始值,\n\n原始值没有引号包裹,也不是预处理字段,随意使用将会带来安全隐患.\n\n\n**注意**\n\n同一个变量不可传入`values()`或`condition()`两次,因为这些方法会修改传入值,第二次执行时,用到的值已经被第一次修改了\n\n可以使用`$update=$insert`,两个变量各自修改不会影响另一个\n\n\n### ORDERLIMIT 构造器\n\n```php\ndb::orderLimit(array $orderLimit)\n```\n\n`$orderLimit`使用关联数组,键为数据库字段,键值为排序规则,`ASC`或`DESC`,也可以使用布尔值代替,`true`为`ASC`,`false`为`DESC`\n\n如`$orderLimit=['id'=\u003e'DESC','name'=\u003e'ASC']`\n\n还可以使用`LIMIT`,添加一个整形的键值对\n\n`$orderLimit=['id'=\u003e'DESC','name'=\u003etrue,35=\u003e20]`\n\n代表`LIMIT 35,20`\n\n直接使用`$orderLimit=['id'=\u003e'DESC','name'=\u003e'ASC',5]`\n\n代表`LIMIT 0,5`\n\n如果要使用`ORDER BY RAND()`\n\n可使用`$orderLimit=[''=\u003e'RAND()']`构造\n\n\n### 使用 ON DUPLICATE KEY UPDATE\n\n`db::insertOrUpdate(string $table, array $insert, array $update): bool`实现了插入或者更新\n\n自己拼接的话\n```php\n$sql = sprintf('INSERT INTO %s ON DUPLICATE KEY UPDATE id=:id,name=:name', self::values($insert, false, static::table));\nreturn self::exec($sql, $insert + $update);\n```\n需要`$update=['id'=\u003e1,'name'=\u003e2];`与自己写的SQL对应\n\n\n### 使用 INSERT DELAYED\n\n\u003e DELAYED 仅适用于 MyISAM, MEMORY 和 ARCHIVE 表\n\n可采用如下方式构造\n\n```php\n$sql=sprintf('REPLACE DELAYED INTO %s',self::values($data,false,static::table));\n$sql=sprintf('INSERT DELAYED INTO %s',self::values($data,false,static::table));\n$sql=sprintf('INSERT DELAYED IGNORE INTO %s',self::values($data,false,static::table));\n```\n\n### 使用`CASE WHEN`\n\n\u003e `CASE WHEN` 可以实现单条SQL语句将多个记录更新为不同的值\n\n\u003e `CASE WHEN` 可以实现对查询的数据对值分组转换等\n\n\n### 批量插入\n\n可以使用`prepare`绑定数据循环.\n\n如果数据表是`InnoDB`而不是`MyISAM`,还可以开启事务,进一步提升速度.\n\n因为`InnoDB`默认是`auto-commit mode`,每条 SQL 都会当做一个事务自动提交,会带来额外开销.\n\n\n`INSERT INTO`也可以使用`IGNORE`,`REPLACE`,`ON DUPLICATE KEY UPDATE`\n\n\n数据源\n\n```php\n$column = ['title', 'text', 'ids', 'enable'];\n$data = [\n\t['title1', 'text1', 'a,b', 1],\n\t['title2', 'text2', 'a,b', 1],\n\t['title3', 'text3', 'b,c', 0],\n\t['title4', 'text4', 'b,c', 0]\n];\n```\n\n`db::insertMany(string $table, array $column, array $data): bool`实现了事务批量插入\n\n\n### 更快的批量插入\n\n使用单条 SQL 代替循环插入速度将会更快\n\n数据源和参数同上\n\n\n`db::insertOnceMany(string $table, array $column, array $data, array $duplicateKeyUpdate = []): int` 实现了单条SQL批量插入，并且可以指定重复键的更新策略\n\n\n_批量插入中使用`ON DUPLICATE KEY UPDATE`仅需配置第四个参数_ ， `$duplicateKeyUpdate`格式同`$column`，需要是其子集\n\n如果数据量巨大，可能造成SQL语句太长，可以使用`array_chunk`切割`$data`分批调用\n\n\n### 嵌套的`AND`和`OR`\n\n对WHERE 构造器传入二维数组，可以构造嵌套的`AND`或`OR`\n\n\n```php\n$where1=['age \u003e'=\u003e18,'sex'=\u003e1];\n$where2=['id \u003e'=\u003e20,'id \u003c'=\u003e40];\ndb::find([$where1, $where2, 'OR'], 'users');\n```\n构造出如下SQL\n```sql\nSELECT * FROM users WHERE ((`age` \u003e :age_1 AND `sex` = :sex_2) OR (`id` \u003e :id_3 AND `id` \u003c :id_4))\n```\n\n\n\n\n### 高级查询\n\n\n如果你需要非常复杂的 SQL 查询,可能不能一次就利用方法完成,需要多次操作\n\n或者自己进行`prepare`并绑定.\n\n使用`db::query`可以一次完成多个 SQL 操作,它是`db::exec`的批处理.\n\n```php\n$sql1=\"SELECT 1\";\n$sql2=\"SELECT 2\";\n$sql3=\"SELECT 3\";\n$data1=$data2=$data3=[];\n[$res1,$res2,$res3]=self::query([$sql1,$data1,'fetchAll'],[$sql2,$data2,'fetch'],[$sql3,$data3,true]);\n```\n\n每个参数都是数组\n\n数组内部,第一个元素要批处理的$sql 语句,第二个参数绑定的参数,第三个参数获取方式.\n\n所有的 SQL 执行最终都会指向`db::exec($sql,array $params=[],$fetch='')`\n\n\n第三个参数`fetch`\n\n如果为空,代表非查询语句,返回的是影响的行数(无预处理`exec()`)或者是否成功(有预处理`stm-\u003eexecute()`)\n\n如果是个`string`,返回结果集(例如`fetchAll`,无论是否走了预处理)\n\n如果是`true`(仅当是查询语句,或者是预处理操作,才能置为true),返回一个`PDOStatement`(无论是否走了预处理),后续按何种方式获取结果集,自己任意操作.\n\n\u003e\n\u003e 注意: params 为空则代表没有预处理参数,底层会直接调用 pdo-\u003eexec() 或 pdo-\u003equery()\n\u003e 如果不为空,则会先预处理,然后stm-\u003eexecute()\n\u003e\n\u003e pdo-\u003eexec() 返回的是一个int值,代表被影响的行数, 例如runSql()函数\n\u003e\n\u003e stm-\u003eexecute() 返回的是一个布尔值,代表是否操作成功,\n\u003e 使用 stm-\u003erowCount() 才能取到执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。\n\u003e\n\u003e 如果你关心受影响的行数,在调用`db::exec`时需注意.\n\n### SQLITE 差异\n\ninsert ignore 和 replace 与MySQL的语法存在差异\n\ninsert or replace：如果不存在就插入，存在就更新\ninsert or ignore：如果不存在就插入，存在就忽略\n\ninsert函数可以新增如下对SQLITE的改写版\n\n```php\nfinal public static function sqliteInsert(array $data, string $table = '', bool $replace_or_ignore = null)\n{\n\t$sql = sprintf('%s %sINTO %s', $replace_or_ignore ? 'INSERT OR REPLACE' : 'INSERT', $replace_or_ignore === false ? 'OR IGNORE ' : '', self::values($data, false, $table));\n\treturn self::exec($sql, $data);\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsuconghou%2Fmvc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsuconghou%2Fmvc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsuconghou%2Fmvc/lists"}