iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
0
自我挑戰組

重新理解 PHP:從頭打造 Web Framework系列 第 8

加入 Symfony Routing 路由元件

前言

在使用 Front Controller Pattern 後,漸漸的會發現到我們的應用程式似乎還不夠靈活。
很多成熟的框架都有良好的 RESTful API 支援,舉例來說,我們可以用 /user/1 代表取得第一位使用者的資料,而 /user/2 取得第二位,以此類推。

然而若依照我們現在框架的寫法,必須為每一位使用者都新增一個路徑,然後渲染出相應的 template,這明顯是不符合效益的。
若是要讓我們的應用程式支援動態路由,除了自己實現以外,我們也可以利用既有的 Symfony Routing 元件達成目標。

加入 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'));

// ... 

此時,我們會需要另外兩個類別 RequestContextUrlMathcer 來幫助我們建立路由的對應關係。(此處假設 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 類別會自動化地幫助我們根據 RequestContextRouteCollection 映射路徑,並以陣列回傳,若未能映射,則拋出 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();

現在,我們的框架具有以下幾項特點:

  • 路由名稱被使用於模板名稱中
  • 正確管理伺服器錯誤(500)

上一篇
整合前端控制器模式
下一篇
控制器(Controller)的設計
系列文
重新理解 PHP:從頭打造 Web Framework9
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言