iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 29
3
Modern Web

成為 Modern PHPer系列 第 29

Day 29:PSR-15 帶來的新生態

前言

昨天我們提到了 PSR-7,認知到 PHP 對於 HTTP 的 Request、Response 有一定程度的標準規範存在(還有一些 Utils,像是 UriInterface

在 PSR-7 漸漸成熟之後,PSR-15 便被提出,為的是強化 PSR-7 的實際應用層面。

概念

對於 HTTP 協定,重點不外乎就是以下兩件事

  • 可以接受請求(Request)
  • 可以根據請求正確回應(Response)

在很多 Web Framework 中都有再多實現一個名為 Middleware 的層級,為的是在 HTTP Request 的生命週期插入一些功能。

Middleware 的概念如下圖所示

https://ithelp.ithome.com.tw/upload/images/20190930/20104201yBrcVYOE9M.png

(圖片來自 Slim Framework 的官方文件)

Middleware 的實作

Koa (Node.js)

const Koa = require('koa');
const app = new Koa();

// Logger middleware
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// Response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Gin (Golang)

package main

import (
    "github.com/gin-gonic/gin"
    "time"
)

func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// Set example variable
		c.Set("example", "12345")

		// before request

		c.Next()

		// after request
		latency := time.Since(t)
		log.Print(latency)

		// access the status we are sending
		status := c.Writer.Status()
		log.Println(status)
	}
}

func main() {
	r := gin.New()
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example").(string)

		// it would print: "12345"
		log.Println(example)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}

Slim (PHP)

<?php

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Psr7\Response;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

/`
 * Example middleware closure
 *
 * @param  ServerRequest  $request PSR-7 request
 * @param  RequestHandler $handler PSR-15 request handler
 *
 * @return Response
 */
$beforeMiddleware = function (Request $request, RequestHandler $handler) {
    $response = $handler->handle($request);
    $existingContent = (string) $response->getBody();

    $response = new Response();
    $response->getBody()->write('BEFORE' . $existingContent);

    return $response;
};

$afterMiddleware = function ($request, $handler) {
    $response = $handler->handle($request);
    $response->getBody()->write('AFTER');
    return $response;
};

$app->add($beforeMiddleware);
$app->add($afterMiddleware);

$app->run();

註:以上範例皆來自於各 Framework 之官方文件

設計理念

藉由「攔截」 HTTP Request 及 Response 的方式,可以在任意位置插入可控的邏輯,又因為 Middleware 應該只包含簡單的邏輯,故其具備較高的可測試性與可重用性。

通常一個 Middleware 會包含幾個要素

  • Request:表示目前接到的請求形式與內容(當中可能已被前面的 Middleware 修改過)
  • Next:表示將繼續往下執行的內容,通常會是一個 Closure,會前往下一個 Middleware

通常來說,當 next() 在後,表示請求時處理;當 next() 在前,表示回應時處理。

PSR-15

概述

PSR-15 分為三個部份

  • RequestHandlerInterface
  • MiddlewareInterface
  • PSR-7 ResponseInterface

外加一個例外處理:Handler Exception

前導知識

為了讓應用程式能夠使用 PSR-15 的功能,我們會需要一個 Dispatcher 負責協調 PSR-15 的各項功能。

在以下的範例中我會使用 relayphp/relay 作為我的 PSR-15 Dispatcher。

用法如下

// $request 為 ServerRequestInterface 的實體
return (new Relay)->handle($request);
// 位於 $middlewares 中的所有項目都是實現了 MiddlewareInterface 的實體
$middlewares = [];

return (new Relay($middlewares))->handle($request);

RequestHandler

RequestHandler 的職責很簡單:接收一個 PSR-7 Request 並回應 PSR-7 Response,通常 RequestHandler 會作為 Middleware 的一部份,但也可以直接成為一個固定形式的功能。

這個介面需要實現一個 public function handle(ServerRequestInterface $request): ResponseInterface

class MarkdownHandler implements RequestHandler
{
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // 確認是否存在 Markdown file
        return file_exists($file = $request->getFile())
            // 存在的話就編譯並作為 Response 回應
            ? $this->compileMarkdown($file)->getResponse()
            // 不存在的話就直接 404 Not Found
            : new Response(404);
    }
}

// $request 已經轉化為 PSR-7 RequestInterface
return (new MarkdownHandler())->handle($request);

Middleware

Middleware 的職責為:指派合適的 PSR-15 RequestHandler 給 PSR-7 Request,並且取得 PSR-7 Response。在此處,RequestHandler 通常是作為類似於 next() 的存在。

這個介面需要實現一個 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface

class AgeLimit implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if ($request->getAge() < 18) {
            return new ApplicationResponse(403);
        }
        
        // 類似於 next() 的存在,指派給下一個 Middleware
        return $handler->handle($request);
    }
}

class Logger implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // 類似於 next() 存在,所以會先執行其它的 Middleware,最後再來執行這個 Logger
        $response = $handler->handle($request);
        
        // 借鑑於實現了 PSR-3 的 Logger Interface
        Logger::debug("Time: {$response->getTime()}");
        
        return $response;
    }
}

$middlewares = [
    AgeLimit::class,
    Logger::class,
];

return (new Relay($middlewares))->handle($request);

資源

對於 PSR-15,可以參考 middlewares/awesome-psr15-middlewares 這個 Repo。

裡面列舉了很多樣的 Dispatcher 或一些常見的功能,雖然數量還不多但對於自己開發仍有一定的參考價值。

後記

今天應該是鐵人賽的最後一天,明天應該會以心得的方式帶過最後一天的內容。

這 29 天算是把一些 Modern PHPer 需要瞭解的東西做了一些整理,不過真正瞭解這些價值的人通常已經不再寫 PHP 了。Golang 或 Nodejs 的薪水要多得多

曾經聽過有人說:給我一個月,我能夠把一個不曾碰過程式的人讓他可以用 PHP 寫出產品。這大概也是 PHP 定位為「簡單易用」的原罪吧,無數的垃圾 Code 就此產生。


上一篇
Day 28:PSR-7 帶來的變革
下一篇
Day 30:鐵人賽總結
系列文
成為 Modern PHPer30

1 則留言

0
EN
iT邦研究生 3 級 ‧ 2019-09-30 13:14:06

那想請問一下芥龍大,若我最近在專題上有開發restful api的需要,會建議我直接跳過php改採node.js開發嗎/images/emoticon/emoticon13.gif
我自己的php不是到很熟,所以還在猶豫要把php弄熟,還是改玩node.js甚至是golang QQ

我要留言

立即登入留言