Page 1
PSR-7とPSR-15による
Webアプリケーション実装パターン
Web application implementation pattern based on PSR-7 and15
pixiv Inc.
USAMI Kenta
2022-04-11 PHPerKaigi Day 2
公開日:
by USAMI Kenta@tadsan
に東京都の練馬区立区民・産業プラザ Coconeriホール およびニコニコ生放送で開催された『PHPerKaigi 2022』でライトニングトーク(5分)として発表しました。
2022-04-11 PHPerKaigi Day 2
お前誰よ
tadsanのあれこれが読める場所
それそろフレームワークとしての突っ込みどころ改善ポイントが枯渇しつつあるので参加したい学生はお早めに
Webフレームワークを作りたい人や内部構造を理解したい人向け
前半はフレームワーク一般の概念の話もします
後半もPSR-15入門とテスト方法みたいな話に終始します
最後のスライド(予告)
(ご静聴ありがとうございました。tadsanの次回作にご期待ください)
誰がPSR-15を使うのか
PSR (PHP Standard Recommendation)
よく知られたPSR
PSR interfaces
インターフェイス(interface) 概要
PSR vs HTTP
├─ RequestInterface(HTTPリクエスト)│ └─ ServerRequestInterface (↑+追加情報)└─ ResponseInterface(HTTPレスポンス)
不変性(immutability)
https://packagist.org/providers/psr/http-message-implementation
PSR-17 HTTP Factories
不変性(immutability)
Nyholm Psr17Factory
PSR-7とフレームワークの関係
<?php$name = $_GET['name'];$title = "Hello, {$name}!";header('Content-Type: application/html; charset=UTF-8');?><title><?= htmlspecialchars($title) ?></title><h1><?= htmlspecialchars($title) ?></h1>
<p>現在は
<time><?= htmlspecialchars(date('Y年m月d日 H時i分s秒')) ?></time>
です</p>
/var/www/example.com/public/index.php
なんかいろんな関数を呼んだりファイルをincludeしたりする
$_SERVER
/var/www/example.com/public/index.php
/var/www/example.com/public/index.php
/var/www/example.com/public/index.php
header()ob_start()
$_SERVER
/var/www/example.com/public/index.php
header()ob_start()echo
$_SERVER
/var/www/example.com/public/index.php
header()ob_start()echo echo
$_SERVER
/var/www/example.com/public/index.php
header()ob_start()echo echoset_cookie()
$_SERVER
/var/www/example.com/public/index.php
header()ob_start()echo echoset_cookie() ob_end_flush()
$_SERVER
こういう世界とどうやって整合性を合わせるのか
PSR-7の出力 (emit)
// 概念的なざっくり実装。ほんとはもうちょっと考慮事項がある
function emitResponse(ResponseInterface $response): void{http_response_code($response->getStatusCode());foreach ($response->getHeaders() as $name => $values) {foreach ((array)$values as $value) {header("{$name}: {$value}");}}
echo $response->getBody();}
PSR-7の入力 (ServerRequestの錬成)
/var/www/example.com/public/index.php
なんかいろんな関数を呼んだりファイルをincludeしたりする
$_SERVER
ServerRequest
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
ServerRequest
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
SapiEmitter
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
SapiEmitter
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER echo header
なんかいろんな関数を呼んだりしてResponse作る
フレームワークの光と影
フレームワークの光と影
フレームワークの光と影
フレームワークの光と影
Long-living PHPの世界
ServerRequest
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
ServerRequest
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
SapiEmitter
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
SapiEmitter
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER echo header
なんかいろんな関数を呼んだりしてResponse作る
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest Response
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest Response
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest Response
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest
SapiEmitter
(fromGlobals)
/var/www/example.com/public/index.php
$_SERVER echo header
なんかいろんな関数を呼んだりしてResponse作る
ServerRequest Response
なんかいろんな関数を呼んだりしてResponse作る
両端の構造が合っていれば従来型とも共通化しやすそう
どこで動いてようがあまり関係なくなる
値を渡して戻り値を検査する
もうちょっと具体的に
PSR-15 HTTP Handlers
ほかの世界のミドルウェア
<?php
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;
interface RequestHandlerInterface{public function handle(ServerRequestInterface $request): ResponseInterface;}
<?php
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;
interface MiddlewareInterface{public function process(ServerRequestInterface $request,RequestHandlerInterface $handler): ResponseInterface;}
class HelloJsonHandler implements RequestHandlerInterface {public function __construct(private StreamFactory $stream_factory,private ResponseFactory $response_factory){}
public function handle(ServerRequest $request): Response {$body = $this->stream_factory->createStream(json_encode(['Hello' => 'World']);return $this->response_factory->createResponse(200)->withHeader('Content-Type', ['application/json'])->withBody($body);}}
// 前略
public function handle(ServerRequestInterface $request): ResponseInterface {if ($request->getMethod() !== 'GET') {return $this->response_factory->createResponse(404);}
// 後はさっきと同じ
$body = $this->stream_factory->createStream(json_encode(['Hello' => 'World']);return $this->response_factory->createResponse(200)->withHeader('Content-Type', ['application/json'])->withBody($body);}
RequestHandler
RequestHandler
テストしてみよう
テストケース自身がFactoryの機能を持っていると書き心地が良くなる
assertEquals()などで比較してみる
RequestHandler
RequestHandler
RequestHandler
create ServerRequest
RequestHandler
ServerRequest
create ServerRequest
RequestHandler
ServerRequest Response
assertEquals()
create ServerRequest
RequestHandler
ServerRequest Response
use Nyholm\Psr7\Factory\Psr17Factory;
trait HttpFactoryTrait{private function psr17factory(): Psr17Factory{return new Psr17Factory();}
public function getRequestFactory(): RequestFactoryInterface{return $this->psr17factory();}
public function getResponseFactory(): ResponseFactoryInterface{return $this->psr17factory();}
public function getServerRequestFactory(): ServerRequestFactoryInterface{
/*** Create a new stream from a string.** The stream SHOULD be created with a temporary resource.** @param string $content String content with which to populate the stream.*/public function createStream(string $contents): StreamInterface{return $this->getStreamFactory()->createStream($contents);}
class HelloJsonHandlerTest extends \PHPUnit\Framework\TestCase{use Helper\HttpFactoryTrait;
private HelloJsonHandler $subject;
public function setUp(): void {parent::setUp();
$this->subject = new HelloJsonHandler($this->getStreamFactory(), $this->getResponseFactory());}
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$actual = $this->subject->handle($request);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
public function requestProvider(): iterable{yield 'GET' => [$this->createServerRequest('GET', '/dummy'),['status_code' => 200,'headers' => ['Content-Type' => ['application/json'],],'body' => '{"Hello":"World"}',],];
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$actual = $this->subject->handle($request);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
$default_expected = ['status_code' => 404, 'headers' => [], 'body' => ''];
$http_methods = ['POST', 'PUT', 'DELETE'];
foreach ($http_methods as $method) {yield $method => [$this->createServerRequest($method, '/dummy'),$default_expected,];}
$default_expected = ['status_code' => 404, 'headers' => [], 'body' => ''];
$http_methods = ['POST', 'PUT', 'DELETE'];
foreach ($http_methods as $method) {yield $method => [$this->createServerRequest($method, '/dummy'),$default_expected,];}
RequestHandlerテストの要点
<?php
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;
interface MiddlewareInterface{public function process(ServerRequestInterface $request,RequestHandlerInterface $handler): ResponseInterface;}
Middleware
RequestHandler
Middleware
RequestHandler
class StrawMiddleware implements MiddlewareInterface{public function process(ServerRequest $request, RequestHandler $handler): Response{return $handler->handle($request);
}}
class StrawMiddleware implements MiddlewareInterface{public function process(ServerRequest $request, RequestHandler $handler): Response{return $handler->handle($request)->withHeader('X-Content-Type-Options', ['nosniff']);}}
class HttpsRedirectMiddleware implements MiddlewareInterface{public function process(ServerRequest $request, RequestHandler $handler): Response{$uri = $request->getUri();if (strtolower($uri->getScheme()) !== 'https') {return $this->response_factory->createResponse(302)->withHeader('Location', [(string)$uri->withScheme('https')]);}return $handler->handle($request);}}
class ErrorPageMiddleware implements MiddlewareInterface{public function process(ServerRequest $request, RequestHandler $handler): Response{try {return $handler->handle;} catch (HttpException $e) {return $this->renderHtmlErrorPage($e);}}}
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$actual = $this->subject->handle($request);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
ResponseHandlerのテスト (再掲)
Middlewareのテスト
Middleware
RequestHandler
Middleware
RequestHandler
Middleware
create ServerRequest
RequestHandler
ServerRequest
Response
assertEquals()
Middleware
create ServerRequest
RequestHandler
ServerRequest
Response
assertEquals()
Middleware
create ServerRequest
RequestHandler
ServerRequest
Response
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$actual = $this->subject->process($request, $handler);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$actual = $this->subject->process($request, $handler);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$ok = $this->createResponse(200);$actual = $this->subject->process($request, new class($ok) implements RequestHandler {public function __construct(private Response $response) {}public function handle(ServerRequest $request): Response {return $this->response;}});
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$ok = $this->createResponse(200);$actual = $this->subject->process($request, new class($ok) implements RequestHandler {public function __construct(private Response $response) {}public function handle(ServerRequest $request): Response {return $this->response;}});
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$ok = $this->createResponse(200);$actual = $this->subject->process($request, new class($ok) implements RequestHandler {public function __construct(private Response $response) {}public function handle(ServerRequest $request): Response {return $this->response;}});
大雑把な処理の流れ
class RawHandler implements RequestHandlerInterface {/*** @psalm-var list<ServerRequest>* @psalm-readonly-allow-private-mutation*/public $received_server_requests = [];public function __construct(private Response $response) {}
public function handle(ServerRequest $request): Response {$this->received_server_requests[] = $request;return $this->response;}
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$ok = $this->createResponse(200);$actual = $this->subject->process($request, new class($ok) implements RequestHandler {public function __construct(private Response $response) {}public function handle(ServerRequest $request): Response {return $this->response;}});
/*** @dataProvider requestProvider*/public function test(ServerRequest $request, array $expected): void{$ok_handler = new RawHandler($this->createResponse(200));$actual = $this->subject->process($ok_handler);
$this->assertSame($expected['status_code'], $actual->getStatusCode());$this->assertEquals($expected['headers'], $actual->getHeaders());$this->assertSame($expected['body'], (string)$actual->getBody());}
public function test(ServerRequest $request, array $expected): void{$ok_handler = new RawHandler($this->createResponse(200));$actual = $this->subject->process($ok_handler);
$this->assertEquals($expected, ['status_code' => $actual->getStatusCode());'headers'=> $actual->getHeaders(),'body' => (string)$actual->getBody(),'not_redirected_count' => count($ok_handler->received_server_requests),);}
public function test(ServerRequest $request, array $expected): void{$ok_handler = new RawHandler($this->createResponse(200));$actual = $this->subject->process($ok_handler);
$this->assertEquals($expected, ['status_code' => $actual->getStatusCode());'headers'=> $actual->getHeaders(),'body' => (string)$actual->getBody(),'not_redirected_count' => count($ok_handler->received_server_requests),);}
class CallbackHandler implements RequestHandlerInterface {/*** @psalm-var list<ServerRequest>* @psalm-readonly-allow-private-mutation*/public $received_server_requests = [];public function __construct(private Closure $callback) {}
public function handle(ServerRequest $request): Response {$this->received_server_requests[] = $request;return ($this->callback)($request);}
class ThrowingHandler implements RequestHandlerInterface {/*** @psalm-var list<ServerRequest>* @psalm-readonly-allow-private-mutation*/Public $received_server_requests = [];public function __construct(private Throwable $e) {}
public function handle(ServerRequest $request): Response {$this->received_server_requests[] = $request;throw $this->e;}
実アプリケーションへの適用を考える
大雑把な処理の流れ
nyholm/psr7-server
大雑把な処理の流れ
nyholm/psr7-server
大雑把な処理の流れ
ちくちく定義する
nyholm/psr7-server
ちくちく定義する
Laminasのエミッタを使う
nyholm/psr7-server
ちくちく定義する
この$middleware何者
Laminasのエミッタを使う
Middleware
RequestHandler
これまではMiddleware1個Handler1個しか考えてない
Middleware
RequestHandler
せっかくの既存ミドルウェアたくさん重ねたくないですか
Middleware
RequestHandler
RequestHandler
// 前略
public function handle(ServerRequestInterface $request): ResponseInterface {$session = $request->getAttribute(Session::class);assert($session instanceof Session);
// $session を使った処理を書く
if ($session instanceof LoggedIn) {// ...}}
どうやってユーザーにコードを書かせるか
ここまでできれば外の枠組みは結構できてきた
アプリケーションってどこにあるの?
いわゆるコントローラやアクション
今回の発表の参照実装とか便利なライブラリとかそのうちいろいろ出します!!!!
(ご静聴ありがとうございました。tadsanの次回作にご期待ください)
