# api **Repository Path**: yu-ang-dev/api ## Basic Information - **Project Name**: api - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-15 - **Last Updated**: 2024-11-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 目标 > 构建以处理 `POST` 请求为主要先决条件的超轻量级 **RESTful** 框架,尽管也支持 `GET` 、 `PUT` 、 `DELETE`和更多 > 当然,我更希望的是开发者,尤其是一些小白,能够像说话一样的去把程序写出来,所以有时候为了实现这个效果不得不走一些弯弯路 :< > Sorry!文档的进度目前赶不上开发进度,但是代码很简单,花半个小时就可以过完。如果想快速了解的话,可以自己看一下`index.php` 和 `demo.php`中的调用事例,`demo.php`是之前用来测试各个功能的测试文件,如果想看到效果需要重命名为`index.php`,现在的`index.php`是目前公司在用的,懒得改了 --- ## 先决条件 需要对NGINX进行配置,这里包含了https的配置,https证书使用`Certbot`生成 ```python server { listen 443 ssl http2; server_name api5.beiruichen.com; index index.php index.html index.htm; root /webroot/path; ssl_certificate #chain/path; ssl_certificate_key #key/path; ssl_session_timeout 5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5"; location / { try_files $uri /index.php$is_args$args; } location ~ \.php { try_files $uri = 404; fastcgi_split_path_info ^(.+\.php)(/.+)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_index index.php; fastcgi_pass 127.0.0.1:9123; } } ``` `ssl_certificate` **#chain/path**; 见https证书生成后的路径 `ssl_certificate_key` **#key/path**; 见https证书生成后的路径 --- ## https证书生成 ### Certbot官网 [https://certbot.eff.org](https://certbot.eff.org/) #### Step.1 这里以MacOS为参考,前提条件是已安装好`brew`,Windows需要下载安装Certbot软件,然后参照第二步运行 ``` brew install certbot ``` 使用以下命令了解`Certbot`命令行详情 ``` certbot -h all ``` 使用以下命令了解`Certbot`关于`nginx`的参数详情 ``` certbot -h nginx ``` #### Step.2 运行命令创建证书 ``` sudo certbot certonly ``` 1. 输入电子邮件地址 ``` Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): 37152420@qq.com ``` 2. 选择证书生成方式 ``` How would you like to authenticate with the ACME CA? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: Apache Web Server plugin (apache) 2: Nginx Web Server plugin (nginx) [Misconfigured] 3: Spin up a temporary webserver (standalone) 4: Place files in webroot directory (webroot) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Select the appropriate number [1-4] then [enter] (press 'c' to cancel): 3 ``` > **这里选择第三种方式** > 需要启动nginx并确保网站可以访问,证书生成期间会验证服务器连通情况 3. 成功的话会提示一下信息 ``` **IMPORTANT NOTES:** - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/api5.beiruichen.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/api5.beiruichen.com/privkey.pem Your certificate will expire on 2021-07-19\. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le ``` > 其中`fullchain.pem`为chain证书文件路径,`privkey.pem`为key文件路径 **MacOS、Linux需要为这两个目录配置访问权限 chmod +x** **事例中的证书有效期至2021-07-19** #### Step.3 配置`nginx.conf`,具体配置参考 [先决条件](#先决条件) --- ## 系统响应状态码 > 参考至HTTP状态响应码 | 概览 | CODE 区间 | 类型 | 含义 | | :-: | :-----: | :---- | :-- | |1**|100-199|信息|服务器接收到请求,需要请求者继续执行操作| |2**|200-299|成功|请求成功并处理| |3**|300-399|重定向|需要进一步的操作以完成请求| |4**|400-499|客户端错误|请求包含语法错误或无法完成的请求| |5**|500-599|服务器错误|服务器在处理的时候发生错误| - 目前已定义状态码为 各类请求的前30个状态码,为系统预留,开发者扩充时请从`23x`、`33x`、`43x`、`53x`顺序开始 ```PHP /// 请求成功但结果非预期类 200 = NULL; /// 请求完成 结果输出 201 = '请求的逻辑处理已完成, 但是未能返回预期结果'; 202 = NULL; /// MySQL 事务的数据回滚 指向 $e->message /// 重定向类 300 = '当前所请求的接口已弃用'; /// 客户端错误类 400 = '当前请求的数据格式或语法有误'; 401 = '请求的方法缺少必要的参数'; 402 = '在当前请求的方法中, 参数类型错误或不匹配'; 403 = '当前请求的令牌有误, 未能获得相关访问权限'; 404 = '请求的方法不存在或未被授权访问'; 405 = '当前请求的参数类型设置有误'; 406 = NULL; /// 对请求进行强验证时返回的错误信息 407 = NULL; /// 数据库表不存在 408 = '令牌已过期, 请重新获取后再试'; 409 = '令牌不存在或已过期, 请重新获取后再试'; 410 = '跳过TOKEN验证的路由设置noauth必须是数组'; /// 服务端多无类 500 = '发生一个未知的系统错误'; 501 = '无法连接数据库'; 502 = '请求的类型尚未在系统中注册或定义'; 503 = 'LEMON\'Z RESTful Server System 依赖Memcached扩展, 但是当前环境并未安装, 如您无控制权限或无需改功能, 请在启动配置里禁用该项'; 504 = '某个对象的创建依赖系统LOG类, 但是它似乎并未启动, 请检查相关配置项'; 505 = '某个类依赖DBH对象, 但是它似乎并未启动, 请检查相关配置项'; 506 = '无效的MySQL事务流程'; 530 = '当前请求使用了类型强验证, 但是并未设置有效的验证规则'; 531 = '当前请求使用了类型强验证, 但是需验证的数据格式有误, 请检查路由设置'; ``` --- # 请求与响应 ## 请求 ### 过滤 > 系统内置了自动过滤器,但仅对请求的参数进行了`strip_tags` 、`trim`处理,更高级处理需要由用户自己完成 ### 过滤返回 > 过滤器是一直默认运行的,为了保持调用的一致性,所有请求的返回值(参数与值)均被转移至 `$this->R` 数组中,同时原超全局变量均被销毁,例如: **POST请求** ```PHP /// 客户端POST上来的请求为 /// $_POST = [ 'username' => 'LEMONZ', 'password' => 'md5password' ] $app->post( '/router', function(){ $username = $this->R['username']; # LEMONZ $password = $this->R['password']; # md5password /// 如果使用超全局变量则会报错 $username = $_POST['username']; # error } ); ``` **GET请求** ```PHP /// 客户端GET上来的请求为 /// $_GET = [ 'username' => 'LEMONZ', 'password' => 'md5password' ] $app->get( '/router', function(){ $username = $this->R['username']; # LEMONZ $password = $this->R['password']; # md5password /// 如果使用超全局变量则会报错 $username = $_GET['username']; # error } ); ``` **DELETE、PATCH、PUT、OPTIONS以及HEAD过滤返回与POST和GET一致** ### 拦截( @20210419-2.0.1 ) > 自定义拦截器可以对输入的参数进行拦截验证,目前内置的参数允许为以下格式 ```PHP any #任意值,跳过拦截 boolean #布尔值 bool #布尔值别名 int #整形 integer #整形别名 domain #域名 host #域名别名 float #浮点数 double #浮点数别名 email #电子邮件 ip #ip mac #电脑MAC地址 url #网站链接 number #数字 username #用户名 nickname #昵称 phone #手机号 password #密码 zh #中文 en #英文 money #货币 zip #邮编 idcard #身份证号 ``` ### 拦截定义 **定义方法与模式** * 简单模式 实例场景模拟 ```PHP # 实例: # 客户端发起的路由请求由2个参数组成,其中 # tid为文章ID,拦截器定义为int整形 # tags为标签名称,拦截器定义为zh即中文 ``` 1. 路由外定义(**推荐**) ```PHP /// 简单模式 $opt = [ 'tid' => 'int', 'tags' => 'zh' ]; $app->get( '/article', function( $opt ){ /// 开始拦截 $this->request->intercept( $opt ); /// code here } ); ``` 2. 路由内定义 ```PHP $app->get( '/article', function(){ $opt = [ 'tid' => 'int', 'tags' => 'zh' ]; $this->request->intercept( $opt ); /// code here } ); ``` 3. 参数定义(**推荐**) ```PHP $app->get( '/article', function( $opt = [ 'tid' => 'int', 'tags' => 'zh' ] ){ $this->request->intercept( $opt ); /// code here } ); ``` * 复杂模式 实例场景模拟 **注:当类型为any时会跳过拦截,如果系统内置的拦截器不满足需求时,可使用该参数或者不对参数进行设置** > 定义方法同简单模式,分为“**路由内、路由外以及参数定义**”,区别在于定义的方式 ```PHP # 实例一: # 客户端发起的路由请求由2个参数组成,其中 # tid为文章ID,拦截器定义为int整形或number,且值为521 # tags为标签名称,拦截器定义为zh即中文,且值为'PHP' ``` ```PHP $opt = [ 'tid' => [ 'type' => [ 'int', 'number' ], 'value' => 521 ], 'tags' => [ 'type' => 'any', 'value' => 'PHP' ] ]; ``` > 复杂模式提供更多可选设置,如下实例,即:可以**仅验证传入参数的类型或仅验证传入参数的值** ```PHP # 实例二: # 客户端发起的路由请求由2个参数组成,其中 # tid为文章ID,拦截器定义为int整形,且值为521 # tags为标签名称,拦截器定义为zh即中文,且值为'PHP'、'Java'或'RESTful'以及更多中的之一 ``` ```PHP $opt = [ 'tid' => [ 'type' => [ 'int', 'username' ] ], 'tags' => [ 'value' => [ 'PHP', 'Java', 'RESTfull' ] ] ]; ``` --- ## 响应 > 响应只有两个方法 `success`、 `error`,都需要手动进行调用,系统内置了一部分状态码,见[系统响应状态码](#系统响应状态码) ### 调用 #### 错误 ```PHP /// 使用内置错误码 $this->error( 404 ); /// 自定义错误码 $this->error( 43X, '错误提示' ); ``` #### 成功 ```PHP /// 使用内置状态码 $this->success( 201 ); /// 自定义状态码 $this->success( 23X, '成功提示' ); ``` ### 返回 > 请求成功时: ```java { code : 2xx, message : '', error : false, data : [ '...' ] } ``` > 请求失败时 ```java { code : 4xx, message : '错误原因', error : true, data : [] } ``` --- # 配置 ## 前置控制配置 `config.lib.php` ## 运行时配置 `options` > 除了前置控制配置,用户也可以在创建实例前,通过`$options`的参数对程序进行简单控制,例如: ```PHP $options = [ 'MCache' => false, /// 不启用Memcached缓存 'log' => false, /// 关闭日志 'noauth' => [ /// 跳过对以下路由的TOKEN验证 '/list', '/category' ] ]; ``` | options | 类型 | 值 | 说明 | 实例 | | :-- | :-- | :-- | :-- | :-- | |noauth| Array | 路由 |当前路由跳过TOKEN验证| $options[‘noauth’] = [ ‘/’, ‘/list’ ] |ModuleName|String|对象名|是否启用某个模块|$options[‘’] = false /// 关闭日志 | # 编程模式 > 如果你的项目需要创建并调用很多接口,建议将相关的接口按照类型分割并打包到不同的文件中,比如:`module/user.api.php => 用户相关接口` 、 `module/mall.api.php => 商城相关接口` > **函数式 或 面向对象的方式,其实都是基于 匿名函数来实现的** > 为了方便区分接口这里对函数接口文件使用 `*.api.php`命名,面向对象的类文件使用 `*.lib.php`,建议统一规范 ## 函数式 ### 函数集文件实例 `module/user.api.php` ```PHP 'int' ] ) { $this->request->intercept( $opt ); $this->success( 201 ); } $updateUserInfo = function() { $data = $this->R; /// other code ... /// $this->sh->update( 'users' )->val( /* 要更新的值 */ )->where()->parse(); $this->success( 200, '用户信息更新成功' ); } /// 其他接口 ?> ``` ### 调用实例 路由入口文件 `index.php` ```PHP get( '/user/info', $getUserInfo ); $app->update( '/user/info', $updateUserInfo ); ?> ``` ## `*` 面向对象 > 推荐使用面向对象的方式进行编程,可以有更好的文件构建与组织 ### 类文件实例 `module/user.lib.php` ```PHP 'int' ] ) { $this->request->intercept( $opt ); $this->error( 404 ); }; } static public function update() { return function() { $data = $this->R; /// other code ... /// $this->sh->update( 'users' )->val( /* 要更新的值 */ )->where()->parse(); $this->success( 200, '用户信息更新成功' ); } } /// 其他的用户操作接口 } ?> ``` ### 调用实例 路由入口文件 `index.php` ```PHP get( '/user/info', \Users\Controller\User::getInfo() ); $app->update( '/user/info', \Users\Controller\User::update() ); ?> ``` --- # 开发 ## sql-helper.lib.php => sh > SQLHelper 类是SQL的快速构建工具,目前支持大部分的**增、删、查、改**操作(底层依靠**dbh**对象来支持),**但是并不支持运行一些运算类的操作**,此类操作还需要调用底层的**dbh**对象来完成 > 以下操作均是建立在数据库中存在相应字段的基础上 > SQLHelper 基于PDO的预处理机制 ### 简单查询 `get` / `fetch` 查询`users`表中所有用户信息 ```PHP $this->get( 'users' )->parse(); ``` 以上语句的运行结果等于 ```MySQL SELECT * FROM users; ``` `get` 与 `fetch` 有两个参数,第一个为表名,第二个为别名,例如: ```PHP $this->sh->get( 'users', 'u' )->parse(); ``` 其运行结果等于 ```MYSQL SELECT * FROM users AS u; ``` `val`方法用在`get` / `fetch` 时,表示要显示的具体列,其参数为数组 ```PHP $this->sh->get( 'users' )->val( [ 'uid', 'username', 'password' ] )->parse(); ``` 其运行结果等于 ```MYSQL SELECT uid, username, password FROM users; ``` 当`val`方法中的数组为 键 => 值 时,则相当于为要查询的列设定了别名即 `列 => 别名`,例如: ```PHP $this->sh->get( 'users' )->val( [ 'uid', 'username', 'password' => 'pwd' ] )->parse(); ``` 其结果等于 ```MYSQL SELECT uid, username, password AS pwd FROM users; ``` > **当查询不调用`val`方法时,默认为查询此表的所有列,即`*`** > `get` 与 `fetch` 方法的区别在于,`get`查询会显示多行数据,`fetch`仅显示一行数据 ### 分库查询 `feat` ```PHP $this->sh->db( 'db1' )->get( 'users' )->parse(); ``` 其结果等于 ```MYSQL SELECT * FROM db1.users; ``` ```PHP $this->sh->db( 'db1' )->get( 'users', 'u' ) ->db( 'db2' )->get( 'mall', 'm' ) ->leftjoin( 'u.uid = m.uid' ) ->parse(); ``` 其结果等于 ```MYSQL SELECT * FROM db1.users as u LEFTJOIN db2.mall as m ON u.uid = m.uid; ``` ### 连表查询 `LEFT JOIN` / `RIGHT JOIN` / `INNER JOIN` ### 条件 #### `WHERE` ```PHP $this->sh->get( 'users' )->where( "uid = ?" )->parse( $uid ); ``` ```PHP $this->sh->get( 'users' )->where( function(){ return "uid = 1" } )->parse(); ``` ```PHP $this->sh->get( 'users' ) ->where( [ "uid != 1 AND username LIKE %g", "AND" => "id = 3 OR username LIKE %a" ] )->parse(); ``` ```MYSQL SELECT * FROM users WHERE ( uid != 1 AND username LIKE %g ) AND ( id = 3 OR username LIKE %a ); ``` #### `LIMIT` ### 排序 #### `ORDER BY` #### `GROUP BY` ### 事务处理 ```PHP /// mysql事务实例 $app->post( '/options', function( $opt = [ 'uid' => 'username' ] ){ $this->request->intercept( $opt ); $before = function() { global $app; $app->put( 'users' )->val( [ 'username' => time() ] )->parse(); $muid = $app->last(); $app->put( 'team' )->val( [ 'muid' => $muid ] )->parse(); }; $after = function() { global $app; $app->response->success( 200, '数据更新成功' ); }; $error = function() { global $app; $app->log::ERROR( $app->dbh->error() ); }; /// todo:这里这样写比较麻烦 /// 建议用 $this->sh->transaction( funciton(){}, function(){} | string, function(){} | string ); 来实现 $this->sh->transaction( $before, $after, $error ); } ); ``` ### 调试 > DEBUG 与 SQL 语句检查 ## database.lib.php => dbh > `dbh`对象是数据库操作的底层支持,其中用于查的方法有 `get` 和 `fetch`,`增、删、改`则全部通过`put`方法进行 > 由于`SQLHelper`助手目前开发进度的局限性,一些更高阶的数据库操作,比如`子查询`、`运算`等,都需要使用`dbh`对象来完成 # 更新日志 ## 2021.04.30-001 * **增加** 一个运行时配置`noauth`项,用于方便用户快速定义跳过TOKEN验证的路由 * **修复** SQLHelper 的 `limit` 方法的逻辑错误 * 如果参数为两个,则分别对应SQL的 `start` 和 `limit` * 如果参数为一个,返回参数指定条目的数据 * **增加** 内置响应状态码,对应`noauth`授权错误