在使用 Front Controller Pattern 後,漸漸的會發現到我們的應用程式似乎還不夠靈活。
很多成熟的框架都有良好的 RESTful API 支援,舉例來說,我們可以用 /user/1
代表取得第一位使用者的資料,而 /user/2
取得第二位,以此類推。
然而若依照我們現在框架的寫法,必須為每一位使用者都新增一個路徑,然後渲染出相應的 template,這明顯是不符合效益的。
若是要讓我們的應用程式支援動態路由,除了自己實現以外,我們也可以利用既有的 Symfony Routing 元件達成目標。
按照慣例,用 composer
加入 Symfony Routing 元件。
composer require symfony/routing
現在你的 composer.json
應該會類似於以下結構。(依照慣例,此處省略了大部份未更動的部份)
{
"require": {
"php": ">=7.0.0",
"symfony/http-foundation": "^4.0",
"symfony/routing": "^4.0"
},
}
為了能夠使用 Symfony Routing,有三個必須使用的元件:
RouteCollection
:定義路由RequestContext
:收集請求資料UrlMacher
:把請求映射(mapping)到單一路由的動作首先移除我們先前自己設計的路由機制。
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
if (isset($map[$path])) {
ob_start();
extract($request->query->all(), EXTR_SKIP);
include $map[$path];
$response->setContent(ob_get_clean());
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
$response->prepare($request)->send();
然後建立路由。(此處假設 Package 已全被 autoload 機制載入)
// rafax/public/index.php
<?php
// ...
$request = Request::createFromGlobals();
$response = new Response();
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', ['name' => 'World']));
$routers->add('bye', new Route('/bye'));
// ...
此時,我們會需要另外兩個類別 RequestContext
及 UrlMathcer
來幫助我們建立路由的對應關係。(此處假設 Package 已全被 autoload 機制載入)
// rafax/public/index.php
<?php
// ...
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', ['name' => 'World']));
$routers->add('bye', new Route('/bye'));
$context = (new RequestContext())->fromRequest($request);
$matcher = new UrlMatcher($routers, $context);
// ...
UrlMatcher
類別會自動化地幫助我們根據 RequestContext
與 RouteCollection
映射路徑,並以陣列回傳,若未能映射,則拋出 Symfony\Component\Routing\Exception\ResourceNotFoundException
,其範例如下。
<?php
print_r($matcher->match('/bye'));
/* Gives:
array (
'_route' => 'bye',
);
*/
print_r($matcher->match('/hello/Vincent'));
/* Gives:
array (
'name' => 'Vincent',
'_route' => 'hello',
);
*/
print_r($matcher->match('/hello'));
/* Gives:
array (
'name' => 'World',
'_route' => 'hello',
);
*/
try {
$mather->match('not-found');
} catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $exception) {
// Not Found.
}
再瞭解了這些知識後,我們可以將原本的程式改寫如下。
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
$request = Request::createFromGlobals();
$response = new Response();
$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', ['name' => 'World']));
$routers->add('bye', new Route('/bye'));
$context = (new RequestContext())->fromRequest($request);
$matcher = new UrlMatcher($routers, $context);
try {
extract($matcher->match($request->getPathInfo()), EXTR_SKIP);
ob_start();
include __DIR__ . "/../Bach/templates/$_route.php";
$response->setContent(ob_get_clean());
} catch (ResourceNotFoundException $exception) {
$response->setStatusCode(404)->setContent('Not Found');
} catch (Exception $exception) {
$response->setStatusCode(500)->setContent('An error occurred');
}
$response->prepare($request)->send();
現在,我們的框架具有以下幾項特點: