在本系列文章的第一部分和第二部分我介紹了一些咱們構建API所須要的基礎庫和基本概念。如今咱們將進入本系列文章的第三部分,在這以前,我想再回顧一下第一和第二部分的內容,總結一些能夠幫助咱們走的更長遠的一些東西。我相信你已經注意到(在這個Git 倉庫中查看本系列文章的「第二部分」的分支上的代碼)在咱們的index.php文件中的代碼量有點大。咱們已經定義了主應用程序,併爲自定義處理程序更改了一些配置選項。即使只是簡單的使用這些代碼,保存到一個文件裏也會變得有點冗長。(以上部分如需查閱可在原文中查閱)php
使用MVC設計模式前端
在這個系列的文章中咱們實現了不少功能,你能夠將這些功能所有保存到一個文件中,不過,這將成爲往後進行代碼維護的「惡夢」。爲了幫助咱們解決便於代碼維護的問題,我將使用一個用來處理大型應用程序的方法:模型/視圖/控制器設計模式。mysql
若是你還不熟悉這種結構,請看下面的簡單介紹:git
· 模型表示要處理的數據。在大多數數據庫驅動的應用程序中,它們將與表直接關聯,每一個實體類型之間都存在着關係。github
· 視圖表示應用程序的輸出,即客戶端的HTML,在咱們本系列文章中的API的輸出是JSON或XML。sql
· 控制器是將模型和視圖綁定在一塊兒的「粘合劑」,並在將值發送到視圖進行輸出以前對值進行一些額外的處理。數據庫
這種結構的目標是基於單一責任原則將應用程序的功能分解成塊。應用程序中的每一個類/對象只能作一件事情。還有其餘的部分被包含在功能更強大的MVC框架中,如服務提供者和其餘業務邏輯處理程序,但咱們在這裏會使用簡單的幾個功能。雖然咱們如今作的事情會涉及到一些額外的處理,但總的來講,咱們將堅持使用純粹的MVC組件。json
咱們將經過一些中間件功能擴展這個MVC結構,這一點咱們在第一部分中簡要的介紹過,讓咱們建立可重複使用的單用途的功能模塊,這樣咱們就能夠在整個系統中重複使用。bootstrap
從個人朋友那獲得的一點幫助設計模式
在PHP生態系統中有大量的MVC框架,咱們可能會使用其中任何一個來完成咱們在這裏作的大部分工做。
正如你已經看到的那樣,Slim框架爲咱們的應用程序提供了最主要的「骨架」,使咱們可以將URL中的請求路由到正確的功能上。正如它的名字同樣,這就是它所帶來的全部功能。還有其餘一些咱們會用到的功能,主要是請求和響應處理。
vlucas/phpdotenv
該庫用於從.env文件讀取定義的內容(默認爲當前目錄)。這些.env文件包含你的應用程序的設置,而且能夠將應用程序的設置保留在代碼以外。而後將它們加載到$_ENV變量中,以便在應用程序中的任何地方均可以輕鬆引用。
aura/session
默認狀況下,Slim是不附帶會話處理程序的,使用PHP本身的$_SESSION功能可能會有點混亂。相反,我已經選擇使用Aura組件集合中的這個包來幫助會話功能保持簡潔。它在$_SESSION內部使用處理程序,因此它仍然使用相同的功能,只是會提供一個友好的界面。
illuminate/database
這是Laravel框架中的數據庫組件,這個組件能使數據庫表中的數據變得更簡單。它是一個ORM(對象關係映射器)工具,它使用ActiveRecord結構來引用數據庫中表示的實體和集合。該軟件包還包括了咱們將用於設置咱們的鏈接的功能——Capsule。
doctrine/dbal
這個庫須要使用Laravel數據庫組件進行一些手動數據庫查詢。雖然從一開始可能不須要這個組件,但若是須要更復雜的查詢,那麼它將會派上用場。
robmorgan/phinx
最後,咱們將安裝Phinx數據庫遷移管理器。這個Illuminate/database包在建立表以後須要處理表的全部事情,但咱們仍然須要建立它們。Phinx能夠輕鬆的根據須要運行或回滾遷移,而且比使用一大堆原始SQL語句更不容易出錯。
要所有安裝以上這些組件,能夠執行下面這條簡單的命令:
> composer require vlucas/phpdotenv aura/session illuminate/database doctrine/dbal robmorgan/phinx
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
[...]
Writing lock file
Generating autoload files
這些軟件包存在着許多其餘的依賴關係,有幾個來自於Symfony和Doctrine。不過不要太擔憂這些依賴關係。即便他們都與Slim一塊兒安裝, vendor/目錄的大小也只有11MB,這比起任何其餘應用程序來講都比較小。
你可能會問,爲何咱們會須要這些程序包?全部這一切難道都不能用簡單的PHP和SQL來完成嗎?這個問題的答案是,這些程序包使得這些功能的開發更快速,由於它們已經通過很好的測試。
「應用程序」結構
如今讓咱們開始構建的過程吧,看看咱們的應用程序將會是什麼樣子的,咱們成功地移動了全部的東西,如今把它分解成各個功能部件。
App/
--> Controller/
--> Model/
--> View/
--> Middleware/
bootstrap/
--> app.php
--> db.php
--> routes.php
templates/
public/
db/
讓咱們一塊兒來看看這個結構。咱們的主要命名空間是App應用程序文件。這是App/目錄下的全部文件,包括控制器,模型和任何可能須要的視圖輔助類文件。在bootstrap目錄的內部,咱們將爲咱們的應用程序提供主要的配置文件。包括了一些基本的應用程序設置(如系列文章第一部分中的處理程序)和Slim應用程序配置。數據庫鏈接信息將存放在db配置文件中,路由設置將在routes配置文件中。
最後的'templates'目錄,能夠存聽任何咱們可能須要的視圖模板,該db目錄將用於存放Phinx遷移的文件,public是放置了咱們的前端控制器index.php文件的目錄。
請注意,咱們正在使用一個子目錄做爲文檔的根目錄。這有助於防止一些安全問題,例如.env中包含的各類敏感信息的文件能夠直接在Web中訪問。
若是你對這些目錄不熟悉,你也不要擔憂,在文章的後面,我將帶你操做每一步,並解釋在任何一步中都發生了些什麼。
如今要花點時間進行目錄的建立:
mkdir App
mkdir bootstrap
mkdir templates
mkdir public
mkdir db
遷移
如今咱們在index.php文件中已經定義了一些代碼:
· 應用程序的引導
· 路由處理
· 根路徑/請求的請求/響應處理程序
構建bootstrap
咱們要把已有的代碼進行修改,並把它們分解成咱們想要的新結構。首先咱們將從bootstrap開始。咱們來看一下這個代碼,把它移到一個bootstrap/app.php文件中,看起來像這樣:
<?php
session_start();
require_once '../vendor/autoload.php';
$dotenv = new DotenvDotenv(BASE_PATH);
$dotenv->load();
$app = new SlimApp();
$container = $app->getContainer();
// Make the custom App autoloader
spl_autoload_register(function($class) {
$classFile = APP_PATH.'/../'.str_replace('', '/', $class).'.php';
if (!is_file($classFile)) {
throw new Exception('Cannot load class: '.$class);
}
require_once $classFile;
});
// Autoload in our controllers into the container
foreach (new DirectoryIterator(APP_PATH.'/Controller') as $fileInfo) {
if($fileInfo->isDot()) continue;
$class = 'AppController'.str_replace('.php', '', $fileInfo->getFilename());
$container[$class] = function($c) use ($class){
return new $class();
};
}
$container['notFoundHandler'] = function($container) {
return function ($request, $response) use ($container) {
return $container['response']
->withStatus(404)
->withHeader('Content-Type', 'application/json')
->write(json_encode(['error' => 'Resource not valid']));
};
};
$container['errorHandler'] = function($container) {
return function ($request, $response, $exception = null) use ($container) {
$code = 500;
$message = 'There was an error';
if ($exception !== null) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
// Use this for debugging purposes
/*error_log($exception->getMessage().' in '.$exception->getFile().' - ('
.$exception->getLine().', '.get_class($exception).')');*/
return $container['response']
->withStatus($code)
->withHeader('Content-Type', 'application/json')
->write(json_encode([
'success' => false,
'error' => $message
]));
};
};
$container['notAllowedHandler'] = function($container) {
return function ($request, $response) use ($container) {
return $container['response']
->withStatus(401)
->withHeader('Content-Type', 'application/json')
->write(json_encode(['error' => 'Method not allowed']));
};
};
這是從咱們以前建立的代碼中複製粘貼的。在這裏,咱們正在建立應用程序,獲取容器並設置咱們的自定義處理程序,用於異常和未找到(404)/不容許(405)的問題。可是,文件開始處有一些額外的代碼須要添加。
首先,在咱們定義以前,你會注意到SlimApp調用了DotenvDotenv和它的load方法。這個方法會在根目錄中的.env查找要加載的文件。我在系列文章中提到過vlucas/phpdotenv這個包,這就是咱們使用它的地方。繼續往下看,在這個項目的根目錄(和public/不是一個級別)中,建立一個名爲.env的文件,文件內容以下:
DB_HOST=localhost
DB_NAME=database_name
DB_USER=database_user
DB_PASS=database_password
以上內容爲咱們提供了咱們稍後設置數據庫鏈接會用到的更新模板。這些值將在運行時經過Dotenv處理程序加載到$_ENV變量中並在整個應用程序中使用。
若是你忘記了設置.env文件或這個文件位於一個錯誤的位置,則該程序包會拋出異常,而且你的應用程序將沒法繼續執行。
接下來讓咱們來看看自定義自動加載器。因爲咱們想要在App應用程序的各個部分中引用命名空間中的類,所以咱們須要添加一個自定義的自動加載器來處理這些請求。咱們利用spl_autoload_register函數來定義這個自動加載器,並使用它的APP_PATH找到匹配的文件。
下面的代碼是Slim在使用控制器時須要的東西。正如我以前提到過的,Slim大量使用依賴注入容器來作不少的事情。這固然也包括了當從路由引用時解析控制器和動做方法。在咱們的根路由示例中,咱們只是直接輸出了一些東西,可是能夠很容易地轉換成以下所示的代碼:
<?php
class IndexController
{
public function index()
{
echo 'index!';
}
}
$app->get('/', 'IndexController:index');
上面定義的GET請求路由是Slim用於將HTTP請求正確的路由到IndexController中的index方法。可是,爲了實現這一點,咱們須要預先加載控制器。DirectoryIterator就是負責預加載的類,它會列出AppController目錄的文件並加載到容器中。這樣就能夠輕鬆的定義咱們的路由了。
編寫前置控制器
如今咱們將把咱們的前置控制器放在public/index.php文件中。由於咱們須要從咱們的引導文件中引入代碼,因此咱們將把它包含在文件的起始位置處,並設置一些咱們之後可使用的其餘常量:
<?php
define('BASE_PATH', __DIR__.'/..');
define('APP_PATH', BASE_PATH.'/App');
require_once BASE_PATH.'/vendor/autoload.php';
// Autorequire everything in BASE_PATH/bootstrap, loading app first - most important
require_once BASE_PATH.'/bootstrap/app.php';
foreach (new DirectoryIterator(BASE_PATH.'/bootstrap') as $fileInfo) {
if($fileInfo->isDot()) continue;
require_once $fileInfo->getPathname();
}
$app->run();
正如你在上面的代碼中看到的,首先咱們定義了能夠跨應用程序使用的兩個常量:BASE_PATH定義了Web應用程序的根目錄(和public/是一個級別的), APP_PATH指向根目錄下的App/文件夾。下面咱們須要使用Composer將 BASE_PATH指向的路徑做爲源進行自動加載。
再往下一點的代碼塊會首先加載咱們先前建立的引導文件bootstrap/app.php,這個文件定義了應用程序和處理程序。而後,使用DirectoryIterator加載bootstrap/目錄中的任何文件。這樣咱們會在後面就可以更容易的添加更多的配置設置,包括咱們的數據庫和路由配置,而無需將它們手動包含在引導文件中。
public/index.php示例文件中的最後一行代碼是調用應用程序對象上的run方法。這個方法是告訴Slim應該處理傳入請求並輸出響應(請求生命週期)的方法。
設置請求路由
如今咱們已經編寫了引導代碼和前置控制器,咱們須要使用新的MVC結構從新定義默認的/根路由。在bootstrap/目錄中建立一個新文件:bootstrap/routes.php。這個文件由咱們的bootstrap/app.php自動加載:
<?php
$app->get('/', 'AppControllerIndexController:index');
爲了從新定義默認的/根路由,須要將/請求指向IndexController。因爲咱們已經將這些控制器注入到了咱們的容器中,所以Slim能夠解析這個文件並將其發送到須要的地方。咱們稍後會在這個控制器中再添加一些功能。如今咱們須要設置一個配置文件和數據庫配置。
定義數據庫配置
如今咱們將建立數據庫配置,利用Laravel's Enloquent包中附帶的「Capsule」功能,就能夠在Laravel應用程序以外使用Eloquent功能。因爲咱們已經使用.env文件定義了咱們的數據庫鏈接信息,因此咱們在這裏須要作的是經過一些代碼來設置"Capsule":
<?php
$dbconfig = [
'driver' => 'mysql',
'host' => $_ENV['DB_HOST'],
'database' => $_ENV['DB_NAME'],
'username' => $_ENV['DB_USER'],
'password' => $_ENV['DB_PASS'],
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
];
$capsule = new IlluminateDatabaseCapsuleManager;
$capsule->addConnection($dbconfig);
$capsule->setAsGlobal();
$capsule->bootEloquent();
我在本教程中使用的是MySQL,但也可使用其餘數據庫。請參閱Laravel手冊以肯定當前支持哪些數據庫。在上面的代碼中,咱們首先從.env文件中定義的$dbconfig數組變量中加載的值來建立數據庫配置。將憑證信息保存在環境變量中能夠防止敏感信息泄露。
最後,咱們經過$capsule對象的addConnection方法建立並傳遞數據庫配置。最後兩行代碼可以使咱們在全局應用程序中無縫地使用Eloquent的功能。
把代碼放在一塊兒
咱們正在進入這個系列最爲重要的部分。因爲咱們以前已經把一些重要的事情準備好了,因此把這些功能合併起來就比較容易了。
咱們先從「base」控制器開始,這個控制器包含了一些簡單的方法,而後咱們能夠在全部的控制器中調用。一些OOP / MVC的純粹主義者可能會不贊同這個想法。建立一個新的文件AppControllerBaseController.php包含以下代碼:
<?php
namespace AppController;
class BaseController
{
protected $container;
/**
* Initialize the controller with the container
*
* @param SlimContainer $container Container instance
*/
public function __construct(SlimContainer $container)
{
$this->container = $container;
}
/**
* Magic method to get things off of the container by referencing
* them as properties on the current object
*/
public function __get($property)
{
// Special property fetch for user
if ($property == 'user') {
return $user = $this->container->get('session')->get('user');
}
if (isset($this->container, $property)) {
return $this->container->$property;
}
return null;
}
/**
* Handle the response and put it into a standard JSON structure
*
* @param boolean $status Pass/fail status of the request
* @param string $message Message to put in the response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonResponse($status, $message = null, array $addl = [])
{
$output = ['success' => $status];
if ($message !== null) {
$output['message'] = $message;
}
if (!empty($addl)) {
$output = array_merge($output, $addl);
}
$response = $this->response->withHeader('Content-type', 'application/json');
$body = $response->getBody();
$body->write(json_encode($output));
return $response;
}
/**
* Handle a failure response
*
* @param string $message Message to put in response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonFail($message = null, array $addl = [])
{
return $this->jsonResponse(false, $message, $addl);
}
/**
* Handle a success response
*
* @param string $message Message to put in response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonSuccess($message = null, array $addl = [])
{
return $this->jsonResponse(true, $message, $addl);
}
}
咱們的BaseController只是定義了一些輔助方法,例如JSON響應的輸出標準化。jsonSuccess和jsonFail只是jsonResponse方法的抽象方法。
另外還定義了__get方法。這是一種PHP魔術方法,當從不存在或不是公開的對象請求屬性時將調用此方法。在這種狀況下,咱們但願可以從容器中得到更多的東西。此外,它還有一些額外的代碼,例如讓用戶註銷會話等。
此外,你還將注意到,咱們正在使用BaseController的__construct方法接收當前容器的初始化實例。Slim在調用控制器時自動執行此操做,這使得基本控制器和擴展它的類均可以訪問到該控制器。
接下來,咱們將建立IndexController來處理/請求,因此AppControllerIndexController.php文件的代碼以下:
<?php
namespace AppController;
class IndexController extends AppControllerBaseController
{
public function index()
{
return $this->jsonSuccess('Hello world!');
}
}
你會注意到咱們已經利用jsonSuccess方法返回了一個 「Hello world!」 。
發起請求
如今,一切都已準備就緒,你能夠經過簡單的HTTP調用來測試調用API的結果。首先,咱們使用以前用過的PHP內置的Web服務器來啓動應用程序:
cd public/
php -S localhost:8000
如今你能夠在瀏覽器中訪問此地址:http://localhost:8000。若是一切順利的話,你應該能夠看到以下響應:
{
success: true,
message: "Hello world!"
}
或者,你也可使用curl來發起請求:
$ curl http://localhost:8000
{"success":true,"message":"Hello world!"}
寫在最後
在這一部分中我作了不少代碼重構的事情,併爲API應用程序增長了複雜性。我知道建立一個「簡單的」API彷佛有點不太可能,可是請相信我,當咱們添加其餘功能時,你就會以爲更容易了。
和以前同樣,你能夠經過查看GitHub倉庫,獲取咱們建立的最新版本的API代碼: https://github.com/psecio/secure-api。master分支是最新的版本,每一個「part *」分支是該系列中每一部分的代碼。若是你在本地建立的代碼中出現了錯誤,請在倉庫中找到正確的代碼,看看它們之間是否存在差別。
最後,咱們須要回顧一下,這個系列的第三部分所作的大部分事情都是在重構應用程序,目的是使得在將來的構建工做中能簡單地整合多個API,爲從此的事情奠基基礎。經過這種重構,咱們能夠開始瞭解一些有趣的事情:用戶登陸的設計以及使用某些中間件來讓工做變得更加簡單。