iT邦幫忙

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

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

控制器(Controller)的設計

前言

如果你有使用其它 PHP 框架的習慣,肯定會覺得我們的框架還少了點什麼。
到目前為止,我們的框架還沒有很複雜的邏輯,如果未來有需要加入比較複雜的邏輯時,我們可能會被迫將邏輯寫在模板中。

這顯然不是個好主意,寫過原生 PHP 的人大多都經歷過 PHP 程式碼與 HTML 混雜在一起時的夢魘。
有鑑於此,我們需要再建立一個新的層(layer),將我們的邏輯放在裡面,通常這個層被稱為控制器(Controller)。

渲染層抽離

目前我們的框架還很小,而且只有一個邏輯:渲染模板。

// 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();

$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);

function render(Request $request): Response
{
    extract($request->attributes->all(), EXTR_SKIP);
    ob_start();
    include __DIR__ . "/../Bach/templates/$_route.php";
    return new Response(ob_get_clean());
}

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func('render', $request);
} catch (ResourceNotFoundException $exception) {
    $response = new Response('Not Found', 404);
} catch (Exception $exception) {
    $response = new Response('An error occurred', 500);
}

$response->prepare($request)->send();

我們嘗試將渲染模板的邏輯分離出來建立一個函式,命名為 render(),並且利用 Request 實體預留的 attributes 屬性存放 $matcher 解析出來的變數。

既然可以利用 call_user_func() 動態地呼叫函式,我們何不試著把函式名稱放在 Route 實體中?

// rafax/public/index.php
<?php

// ... 

$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', [
    'name' => 'World', 
    '_controller' => 'render', 
]));
$routers->add('bye', new Route('/bye', [
    '_controller' => 'render', 
]));

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request)
}

// ... 

除了在 Route 實例中的 _controller 屬性使用字串外,你可能會更偏好使用一個 callback ,如此一來能對 RequestResponse 做更詳細的操作。

// rafax/public/index.php
<?php

// ... 

$routers = new RouteCollection();
$routers->add('hello', new Route('/hello/{name}', [
    'name' => 'World', 
    '_controller' => function (Request $request) {
        return render($request);
    }, 
]));
$routers->add('bye', new Route('/bye', [
    '_controller' => function (Request $request) {
        // 在 $request 中加入 $foo = 'bar' 的變數
        $request->attributes->set('foo', 'bar');
        
        $response = render($request);
        
        // 變更 $response 的 Header
        $response->headers->set('Content-Type', 'text/plain');
        return $response;
    }, 
]));

try {
    $request->attributes->add($matcher->match($request->getPathInfo()));
    $response = call_user_func($request->attributes->get('_controller'), $request)
}

// ... 

新增邏輯

我們把渲染層邏輯成功抽離後,再嘗試著加入一個新的邏輯:取得現在的 unix timestamp 並且轉為日期字串格式。

// rafax/public/index.php
<?php

// ...

$routers = new RouteCollection();
$routers->add('time', new Route('/time', [
    '_controller' => function (Request $request) {
        $time = new DateTime();
        $response = new Response(
            "Timestamp: {$time->getTimestamp()}\nDatetime: {$time->format('Y-m-d H:i:s')}"
        );
        $response->headers->set('Content-Type', 'text/plain');
        return $response;
    },
]));

// ...

重構

雖然我們新加了一層 Controller,框架的功能變得比以往強大,但同時也變得比以往來得凌亂。
此時我們就要靠著重構來強化框架的系統性。

重構 Hello 與 Bye

// rafax/Bach/app/Http/Controllers/HelloController
<?php

namespace Bach\Http\Controllers;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function render(Request $request): string
    {
        extract($request->attributes->all(), EXTR_SKIP);
        ob_start();
        include __DIR__ . "/../../../templates/$_route.php";
        return new Response(ob_get_clean());
    }
}

// rafax/public/index.php
<?php

// ...

$routers->add('hello', new Route('/hello/{name}', [
    'name' => 'World',
    '_controller' => [new HelloController(), 'render'],
]));
$routers->add('bye', new Route('/bye', [
    '_controller' => [new HelloController(), 'render'],
]));

// ...

重構 Time

// rafax/Bach/app/Http/Controllers/TimeController
<?php

namespace Bach\Http\Controllers;

use Datetime;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class TimeController
{
    public function time(Request $request): Response
    {
        $time = new DateTime();
        $response = new Response(
            "Timestamp: {$time->getTimestamp()}\nDatetime: {$time->format('Y-m-d H:i:s')}"
        );
        $response->headers->set('Content-Type', 'text/plain');
        return $response;
    }
}

// rafax/public/index.php
<?php

// ...

$routers->add('time', new Route('/time', [
    '_controller' => [new TimeController(), 'time'],
]));

// ...

經過重構後,會發現我們的框架整齊多了,而且易於閱讀。
其實截至目前為止,我們的框架已經可以建立簡單的網站,同時也是個簡易的 MVC 架構--請原諒我這麼形容,雖然 MVC 這個詞不太精準,但畢竟它對於推廣框架有一定程度的幫助。


上一篇
加入 Symfony Routing 路由元件
系列文
重新理解 PHP:從頭打造 Web Framework9
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言