PHP應用程序在MVC模式中構建安全API

 

在本系列文章的第一部分和第二部分我介紹了一些咱們構建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,爲從此的事情奠基基礎。經過這種重構,咱們能夠開始瞭解一些有趣的事情:用戶登陸的設計以及使用某些中間件來讓工做變得更加簡單。

相關文章
相關標籤/搜索