<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 用restful的原因 現在移動應用越來越流行,web后端也被要求能開發api了。而api的話restfull 相比web socket和 web service 、soap之類的更簡單和簡潔。實現起來也最容易。 而ThinkPHP3.1 版本就支持了RestController 支持restfull api 開發。 ## 本人經歷 而本人曾嘗試過一次用restController開發 一個機票應用。后來在jobdeer又用了另外一個api 框架lazyPHP4 開發jobdeer項目的api。 所以對于api這邊應該還算有點了解。 但是,看了 [《RESTful API 設計指南》](http://www.ruanyifeng.com/blog/2014/05/restful_api.html) 后,覺的我們對api理解還不夠深入,或者沒有做到最好。 ## 他人吐槽 外加官網曾有人發帖[《用Thinkphp做不到Restful的URL風格》](http://www.thinkphp.cn/topic/28423.html) ,我覺的官方一直沒有提供一個好的restfull 實現案列。 所以我在本項目實戰里特意去研究和使用最新版restController,希望能分享我的理解。 首先,我把他的需求理解一下,第一要支持rest方法請求類型,第二要直接User前面不帶任何其他的Api參數。 其實他的問題省略Api那個,要么寫空控制器,要么用路由,就能把一些url映射過去訪問。 其他的本來就可以實現。 而我之前和歸歸有過一次關于api的討論。他吐槽了好多。。 他們是麥客瘋app,用TP開發的手機api。 1.要維護多個版本。 ![2015-06-10/5577fc9c974f2](http://box.kancloud.cn/2015-06-10_5577fc9c974f2.png) 其實好多接口版本比例不足1%,某些低版手機app版本應該舍棄的。 2.沒有返回狀態碼,請求成功都是200。 我在《HTPP權威指南里》看到 “post時新增 成功 201” ,“更新時 如果發現 依賴條件缺乏不處理 412”等,這樣程序員可以方便定位邏輯錯誤和數據錯誤。 3.權限問題 他們數據加密了。 4.規范問題 如coding的 是 [raml](https://coding.net/u/baoti/p/Coding-API/git) ![2015-06-10/557801ced72b6](http://box.kancloud.cn/2015-06-10_557801ced72b6.png) breeze 里用的微軟的 odata。 5.數據級聯問題 在jobdeer里,羅飛的要求是盡量在一個接口里搞定,讓客戶端少請求。然后業務和數據雜糅。一個接口寫了好長。有時還會調用第三方服務。 而我理解的是應當面向資源。 6.接口測試 我們公司的是用phpunit單元測試做黑盒測試,測試接口訪問性,返回參數 響應code 是不是對。后來接口規模上去了,300多個,每次跑測試2分鐘。如果要按他們6個版本算就是1800個。殺了我吧。 他們公司是人工測試 手動點app觸發。 ## 自己的實現 所以總結了上面的各種情況,我希望我實現的restfull能做到以下幾點: - url要短,簡潔 類型都不在url里了能不短嗎? - 安全考慮 - 面向資源 - 區分返回狀態碼 - 提示統一 - 復用代碼 - 返回格式固定 ### 接口的調試 在開發接口時,我們經常想知道,傳給接口的參數變量是什么,以及接口里各個分支是否運行到,還有最后的操作數據庫sql是什么,如果我們隨時dump,會破壞接口的返回,一般是不允許的。所以我們需要一個工具來調試這些信息,原始點可以寫日志,效率高點,就要借助之前我們已經學習過調試異步的好工具“Socketlog”了。 而對于接口表單的構建,簡單點,可以js里 jquery ajax 調用。當然實際上頁面里也是這么做,當我們頁面還沒有時,其實就已經可以開發接口了。 這可以借助于“Postman”。Chrome應用。 ![2015-06-10/55780a02a5b07](http://box.kancloud.cn/2015-06-10_55780a02a5b07.png) 他本身就是一個rest client。可以支持rest的請求方法。支持多個參數,響應支持預覽xml和json。 最主要的是可以收藏你的一次測試請求包括參數url等各種輸入。另外文件上傳也可以在參數里選擇: ![2015-06-10/55780a8ad2e02](http://box.kancloud.cn/2015-06-10_55780a8ad2e02.png) 總之就是一個神器,還跨平臺。 ### 我的實現 #### 代碼復用 我認為接口應該單獨做為一個模塊,不應該放入主應用模塊中,這樣方便以后好擴展。只要我的表設計好,api模塊,隨時可以拷貝到別的項目中,然后那個項目再開發一個前臺和后臺就完事了。 因此,我采用了下面的結構: ![2015-06-11/5578d08116222](http://box.kancloud.cn/2015-06-11_5578d08116222.png) Api+Common 模塊,Api負責rest,Common負責公共模型和函數。 #### URL短一點,再短一點 之前那個同學說 無法實現 `GET users` 而只做到了 `http://demo.com/Api/User` 這樣的地址,其實很好理解。他用了Api控制器繼承restController了。因此URL里必須有Api。 那么如何省略Api呢?最好的方法是用空控制器。用路由太麻煩了,每增加一個路由,就得增加一個配置項。 再配合.htaccess 文件隱藏入口。就已經做到了 `GET users`能進入空控制器了。 #### 整個控制器代碼 為了方便大家理解代碼,我先將整個代碼放在這,后面一個片段一個片段的講。 ~~~ <?php namespace Api\Controller; use Think\Controller\RestController; class EmptyController extends RestController{ protected $allowMethod = array('get','post','put','delete'); // REST允許的請求類型列表 protected $allowType = array('json'); // REST允許請求的資源類型列表 protected $defaultType = 'json'; protected $allowOutputType = array( 'json' => 'application/json', ); protected $otherResource = array( 'pic', 'file', 'config', ); public function _initialize(){ $this->resource_name = strtolower(CONTROLLER_NAME); $this->messages = array( 'get' => '獲取', 'put' => '更新', 'post' => '新增', 'delete' => '刪除', ); $config = S('DB_CONFIG_DATA'); if (!$config) { /* 讀取站點配置 */ $map = array('status' => 1); $configModel = D('Config'); $data = $configModel->where($map)->field('type,name,value')->select(); $config = array(); if ($data && is_array($data)) { foreach ($data as $value) { $config[$value['name']] = $configModel->parse($value['type'], $value['value']); } } S('DB_CONFIG_DATA', $config); } C($config); //添加配置 } public function _empty($name){ $table = $this->resource_name; if(!in_array($table, $this->otherResource)){ //先判斷表存不存在 if(!M()->query("SHOW TABLES LIKE '".C('DB_PREFIX')."{$table}'")){ $this->response(array('code'=>404, 'message'=> "Resource '{$this->resource_name}' doesn't exist"), $this->defaultType, 404); } }else{ if(method_exists($this, $table)) $this->$table($name); } $model = D(ucfirst($table)); $result = true; $data = array(); $code = 404; $url = ''; switch ($this->_method){ case 'head': break; case 'option': break; case 'get': // 列出資源 if('list' == $name){ $data = $model->select(); }else{ $id = intval($name); $data = $model->find($id); } if($model->getError() || $model->getDbError()){ $result = false; }else{ $code = 200; } break; case 'put': // 更新資源 $puts = $model->create(I('put.')); if(false === $puts){ $result = false; $data = $model->getError(); }else{ $id = intval($name); if($find = $model->find($id)){ $result = false !== $model->save($puts); $code = $result? 200: 404; }else{ $result = false; $data = "record not found"; $code = 412; } } break; case 'post': // 新增資源 $posts = $model->create(); if(false == $posts){ $data = $model->getError(); $result = false; }else{ $id = $model->add(); if(!$id){ $result = false; }else{ $code = 201; $data = $id; } } break; case 'delete':// 刪除資源 $id = I('get.id',0); slog($id); if($find = $model->find($id)){ $result = $model->delete($id); $code = $result? 200: 404; $url = $_SERVER['HTTP_REFERER']; }else{ $result = false; $data = "record not found"; $code = 412; } break; } if($result){ $this->success($data, $code, $url); }else{ $this->error($data, $code, $url); } } public function config($name = 0){ $model = D('Config'); $result = true; $data = array(); $code = 404; $url = ''; switch ($this->_method){ case 'head': break; case 'option': break; case 'get': // 列出資源 if('list' == $name){ $data = $model->select(); }else{ $id = intval($name); $data = $model->find($id); } if($model->getError() || $model->getDbError()){ $result = false; }else{ $code = 200; } break; case 'put': if('save' == $name){ // 批量更新資源 $config = I('put.config'); if(empty($config)){ $result = false; $data = '表單為空'; }else{ if($config && is_array($config)){ foreach ($config as $name => $value) { $map = array('name' => $name); $model->where($map)->setField('value', $value); } } S('DB_CONFIG_DATA',null); $code = 200; } }else{ $puts = $model->create(I('put.')); if(false === $puts){ $result = false; $data = $model->getError(); }else{ $id = $puts['id']; if($find = $model->find($id)){ $result = false !== $model->save($puts); $code = $result? 200: 404; }else{ $result = false; $data = "record not found"; $code = 412; } } } break; case 'post': // 新增資源 $posts = $model->create(); if(false == $posts){ $data = $model->getError(); $result = false; }else{ $id = $model->add(); if(!$id){ $result = false; }else{ $code = 201; $data = $id; $url = '/admin.php/Config/index'; } } break; case 'delete':// 刪除資源 // parse_str(file_get_contents('php://input'), $_DELETE); // slog($_DELETE); $id = array_unique((array)I('get.id',0)); slog($id); if ( empty($id) ) { $code = 404; $data = '請選擇要操作的數據'; }else{ $code = 200; $map = array('id' => array('in', $id) ); if(M('Config')->where($map)->delete()){ S('DB_CONFIG_DATA',null); //記錄行為 $url = '/admin.php/Config/index'; $data = '刪除成功'; } else { $code = 412; $result = false; $data = '刪除失敗!'; } } break; } if($result){ $this->success($data, $code, $url); }else{ $this->error($data, $code, $url); } } public function success($data, $code=200, $url=''){ $response = array( 'code'=>$code, 'data'=>$data, 'info'=>$this->response_info($this->resource_name, $this->_method, 'succeed') ); if($url) $response['url'] = $url; $this->response($response, $this->defaultType, $code); } public function error($data, $code=404, $url=''){ $response = array( 'code'=>$code, 'info'=>$this->response_info($this->resource_name, $this->_method, 'failed') ); if($data) $response['info'] .= ". 原因: {$data}"; if($url) $response['url'] = $url; $this->response($response, $this->defaultType, $code); } private function response_info($resource, $method, $flag){ static $resource_name_strings = array( 'post' => '博文', 'config' => '配置', 'file' => '文件', 'picture' => '圖片', 'member' => '用戶', 'message' => '消息', 'sns' => '第三方登錄賬號', 'tags' => '標簽', 'url' => '外鏈', ); $action_strings = $this->messages; $resource_name = isset($resource_name_strings[$resource])? $resource_name_strings[$resource] : $resource; $action = isset($action_strings[$method])? $action_strings[$method] : $method; $action_flag = 'succeed' == $flag ? '成功' : '失敗'; return sprintf('%s%s%s', $action, $resource_name, $action_flag); } } ~~~ #### 分解講解 1.REST模式的定制化 首先rest是支持多種方法和返回類型的。為了簡化問題,我演示的這個應用約定rest接口管 'get','post','put','delete' 四個方法,允許請求和輸出的類型都是json。 因此有下面的屬性 ~~~ protected $allowMethod = array('get','post','put','delete'); // REST允許的請求類型列表 protected $allowType = array('json'); // REST允許請求的資源類型列表 protected $defaultType = 'json'; protected $allowOutputType = array( 'json' => 'application/json', ); ~~~ 因為從開發角度來講,api支持的模式越多越好,但是從項目管理的角度來講,支持xml等格式,代表我前端代碼里要寫這些請求,這樣整個項目里有2種或兩種以上的請求方式,給項目造成混亂,且效率不高。 > 問題如果經過的步驟越多,出問題的機率越高。 我的需求我來定。就json了,這是目前最流行的格式。 ~~~ protected $otherResource = array( 'pic', 'file', 'config', ); ~~~ 這邊先記一下,其他可獲取處理的資源。后面講empty方法時會說明。 2.初始化操作處理 ~~~ public function _initialize(){ $this->resource_name = strtolower(CONTROLLER_NAME); $this->messages = array( 'get' => '獲取', 'put' => '更新', 'post' => '新增', 'delete' => '刪除', ); $config = S('DB_CONFIG_DATA'); if (!$config) { /* 讀取站點配置 */ $map = array('status' => 1); $configModel = D('Config'); $data = $configModel->where($map)->field('type,name,value')->select(); $config = array(); if ($data && is_array($data)) { foreach ($data as $value) { $config[$value['name']] = $configModel->parse($value['type'], $value['value']); } } S('DB_CONFIG_DATA', $config); } C($config); //添加配置 } ~~~ 初始化方法里,我做了3件事。將控制器方法全部轉小寫賦值給`resource_name`這個類屬性、定義了messages操作提示說明屬性、參照OneThink里將后臺的配置合并到項目中(并加了緩存)。 3. 核心empty 方法 我的rest主要邏輯就在empty方法中。 整體的結構是 1. 資源網關判斷 2. 根據請求類型獲取數據 3. 返回資源數據 ##### 資源網關判斷 ~~~ $table = $this->resource_name; if(!in_array($table, $this->otherResource)){ //先判斷表存不存在 if(!M()->query("SHOW TABLES LIKE '".C('DB_PREFIX')."{$table}'")){ $this->response(array('code'=>404, 'message'=> "Resource '{$this->resource_name}' doesn't exist"), $this->defaultType, 404); } }else{ if(method_exists($this, $table)) $this->$table($name); } ~~~ 首先獲取資源名(已經全部小寫了),然后去其他資源里檢索,不存在的話判斷該資源是數據庫表結構類型資源,然后查表,看該表存在不存在。不存在直接報錯(常見的非法請求資源)。 存在的話訪問后面的。而其他資源網關白名單里,資源存在的話,如果存在資源方法,執行自定義的資源方法。 就是 `$this->$table($name);`,這句。為什么要這一句?為了復用代碼。 官方的里面,針對某一資源的不同請求類型,是需要定義不同方法的。 假設像下面的: ~~~ Public function read_get_json(){ // 輸出id為1的Info的html頁面 } Public function read_post_json(){ // 新增數據 } Public function read_put_json(){ // 更新數據 } Public function read_delete_json(){ // 刪除數據 } ~~~ 我那樣寫是支持一個地址,一個方法覆寫4種類型,這樣其實 更新和獲取資源里的查詢方法可以復用。總之方法靈活了不少。 ##### 請求資源 ~~~ $model = D(ucfirst($table)); $result = true; $data = array(); $code = 404; $url = ''; switch ($this->_method){ case 'head': break; case 'option': break; case 'get': // 列出資源 if('list' == $name){ $data = $model->select(); }else{ $id = intval($name); $data = $model->find($id); } if($model->getError() || $model->getDbError()){ $result = false; }else{ $code = 200; } break; case 'put': // 更新資源 $puts = $model->create(I('put.')); if(false === $puts){ $result = false; $data = $model->getError(); }else{ $id = intval($name); if($find = $model->find($id)){ $result = false !== $model->save($puts); $code = $result? 200: 404; }else{ $result = false; $data = "record not found"; $code = 412; } } break; case 'post': // 新增資源 $posts = $model->create(); if(false == $posts){ $data = $model->getError(); $result = false; }else{ $id = $model->add(); if(!$id){ $result = false; }else{ $code = 201; $data = $id; } } break; case 'delete':// 刪除資源 $id = I('get.id',0); slog($id); if($find = $model->find($id)){ $result = $model->delete($id); $code = $result? 200: 404; $url = $_SERVER['HTTP_REFERER']; }else{ $result = false; $data = "record not found"; $code = 412; } break; } ~~~ 如果資源非自定義資源,就走通用資源處理邏輯,就是上面的代碼。 一般都能看懂。 先獲取模型,然后請求方法類型,get的話是獲取數據。大家注意empty里的$name 這個實際上獲取的是 資源URL 第一個**/** 分割后的字符串。 比方說 `get user` 原本$name 會為空 但是我Api模塊配置里`DEFAULT_ACTION`默認了list ,所以會是list;而 `get user/1` $name 為1。 這代碼中的list 是我在配置里定義的。 ~~~ <?php return array( 'URL_HTML_SUFFIX' => '', 'DEFAULT_ACTION' => 'list', ); ~~~ 這樣符合實際意義。沒有參數就是獲取全部列表。 然后就是更新和刪除時限查找原始數據, 沒有原始數據,響應碼是不一樣。 ##### 發送響應 ~~~ if($result){ $this->success($data, $code, $url); }else{ $this->error($data, $code, $url); } ~~~ empty 方法中最后返回了響應,為了和以前非api編碼方式保持一致,我覆寫了控制器里 success和error方法 ~~~ public function success($data, $code=200, $url=''){ $response = array( 'code'=>$code, 'data'=>$data, 'info'=>$this->response_info($this->resource_name, $this->_method, 'succeed') ); if($url) $response['url'] = $url; $this->response($response, $this->defaultType, $code); } public function error($data, $code=404, $url=''){ $response = array( 'code'=>$code, 'info'=>$this->response_info($this->resource_name, $this->_method, 'failed') ); if($data) $response['info'] .= ". 原因: {$data}"; if($url) $response['url'] = $url; $this->response($response, $this->defaultType, $code); } ~~~ success里第一個參數是返回數據,后面是響應碼,最后是可選的url參數,那篇文章里有 Hypermedia API的概念,預留起來作為跳轉url也行。 error里 data是用來作補充原因說明的,因為錯誤響應應該不需要返回太多數據。 響應兩個方法里用到了 一個私有方法 response_info。 ~~~ private function response_info($resource, $method, $flag){ static $resource_name_strings = array( 'post' => '博文', 'config' => '配置', 'file' => '文件', 'picture' => '圖片', 'member' => '用戶', 'message' => '消息', 'sns' => '第三方登錄賬號', 'tags' => '標簽', 'url' => '外鏈', ); $action_strings = $this->messages; $resource_name = isset($resource_name_strings[$resource])? $resource_name_strings[$resource] : $resource; $action = isset($action_strings[$method])? $action_strings[$method] : $method; $action_flag = 'succeed' == $flag ? '成功' : '失敗'; return sprintf('%s%s%s', $action, $resource_name, $action_flag); } ~~~ 這個方法是用來友好提示的。本來想,就用 資源+動作+結果-> post get succes。這樣的,后來一想 前臺給人用,還要參加coding Html5比賽,干脆格式化一下。 整個restful 實現就是這樣,最后再加上api.php入口的參數綁定, ~~~ <?php define('APP_PATH','./App/'); define('APP_DEBUG', 1); define('BIND_MODULE','Api'); if(!function_exists('slog')){ require './SocketLog.class.php'; $slog_config=array( 'host'=>'i.kuaijianli.com', 'port'=>1229, 'error_handler'=>true, 'optimize'=>true, 'allow_client_ids'=>array('yangweijie_jay'), 'show_included_files'=>false ); if(isset($_GET['slog_force_client_id'])){ $slog_config['force_client_id'] = $_GET['slog_force_client_id']; } slog($slog_config,'set_config'); } require './ThinkPHP/ThinkPHP.php'; ~~~ 至于config方法,可以先不看,只是我針對后臺配置定義的接口,實現OneThink里 配置管理。 ##### 權限的思考 有的時候我們接口會有一些需求,某數據所有者才能操作。有的數據某些條件才能操作。所以我選擇了多入口,api入口去做。這樣的好處是什么?共享session。因為只是入口不同,域名一樣,session完全可以共享。 這樣,我在`/App/Api/Common/function.php`里只定義了一個is_login函數。 ~~~ /** * 判斷是否登錄,如果登錄了返回uid */ function is_login(){ return session('?user')? session('user.uid'): 0; } ~~~ 如果有權限需要,接口里可以加一層登錄網關判斷,添加不必要登錄請求接口網關就可以了。 至于關聯數據,我們TP有模型。可以after 后置 select|find|update|delete。 這些不是要擔心的。 #### 關于代碼風格 所有代碼不超過350行。 我不是不喜歡注釋,但是覺得有時候自注釋的代碼才是好的,明明有英文方法名和參數名,能把你的目的表達出來。額外的添加注釋,是為了照顧不懂英文的新人嗎? 我記得一個關于編碼不會出錯一段話,大意是,有兩種方式保證你寫代碼不會出錯:1.將代碼寫的簡答的誰都能懂,毫無疑問的不出錯;2.另外一種是把代碼寫的復雜到沒人看懂的不出錯。 我認為代碼的實現就應該簡單、簡潔,一眼能懂。編程語言也是門語言。你寫代碼是為了計算機運行。雖然有時候大師寫的代碼很簡潔。但是對于計算機來說結果正確,人人能看懂你意圖的代碼就是好代碼。就好比你給一個女子寫信,不論你文辭多么好,有詩意,你用文言文說“窈窕淑女,君子好逑”和“美女啊,我喜歡你”效果是不一樣的。表達意思一樣,但就理解的人來說,明顯后面的人要多一些。 ## 后話 我喜歡rest,因為他把后端的事情簡化了。只有curd。沒有太多的復雜邏輯在控制器里。后端做的事就是設計好數據庫、寫好控制器和繼續學習把。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看