[php] Slim4 とその周辺についてのコードリーディング
Prerequisite
PHPでのAPIサーバー実装チョットワカル人向け。
Overview
Slim, a micro framework for PHP
slimphp
では、Slim
以外にも Slim-Psr7
など周辺のコンポーネントの実装も行われている。
Middleware (PSR-15)
は独自実装、
HTTP Message (PSR-7)
は Slim-Psr7
,
Dependency Injection
は PHP-DI
,
Router
は nikic/FastRoute
をデフォルトとしている。
Lifecycle
Slim さん優しい。とりあえずこれを見るべし。
曰く、Slim のライフサイクルは- Slim App のインスタンス化。
$app = new App();
a. (追記)DI Container の set などもここで行う - Route の登録。
$app->get('hello', Hello::class);
a. (追記)実際はここで$app->add($middleware);
などして、Middlewareを追加する - 実行。
$app->run()
a. Middleware を stack に積む。tip (先端) から実行(後述)
b. Dispatch された Route の API Handler を実行する
c. Middleware の stack を遡っていく。ここはコード見た方が理解が早いので後述
d. Response の送信。
という流れである。それはそうなのだが 3 あたりで急に情報の密度が上がり置いていかれるので順に説明する。
1_new App()
PSR-17: HTTP Factories 準拠のため、 AppFactory::create()
でApp が作成される。App とは Slim Framework の User Interface と思ってもらえれば。
Slim の特徴として、コンストラクタで周辺コンポーネントオブジェクトの初期化を行っているので、__construct()
までしっかり読む必要あり。
余談だが、Slim ユーザーとしては Slim の実装でも DI を使いたいものだと思った。
AppFactory::create()
, App::__construct()
あたりを見ると、Slim がどんな周辺とでも接続(Glue)できるように、GlueCode がたくさんあることがわかる。
public static function create(
?ResponseFactoryInterface $responseFactory = null,
?ContainerInterface $container = null,
?CallableResolverInterface $callableResolver = null,
?RouteCollectorInterface $routeCollector = null,
?RouteResolverInterface $routeResolver = null,
?MiddlewareDispatcherInterface $middlewareDispatcher = null
): App {
static::$responseFactory = $responseFactory ?? static::$responseFactory;
return new App(
self::determineResponseFactory(),
$container ?? static::$container,
$callableResolver ?? static::$callableResolver,
$routeCollector ?? static::$routeCollector,
$routeResolver ?? static::$routeResolver,
$middlewareDispatcher ?? static::$middlewareDispatcher
);
}
public function __construct(
ResponseFactoryInterface $responseFactory,
?ContainerInterface $container = null,
?CallableResolverInterface $callableResolver = null,
?RouteCollectorInterface $routeCollector = null,
?RouteResolverInterface $routeResolver = null,
?MiddlewareDispatcherInterface $middlewareDispatcher = null
) {
parent::__construct(
$responseFactory,
$callableResolver ?? new CallableResolver($container),
$container,
$routeCollector
);
$this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector);
$routeRunner = new RouteRunner($this->routeResolver, $this->routeCollector->getRouteParser(), $this);
if (!$middlewareDispatcher) {
$middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $this->callableResolver, $container);
} else {
$middlewareDispatcher->seedMiddlewareStack($routeRunner);
}
$this->middlewareDispatcher = $middlewareDispatcher;
}
上記のように、Container から Middleware まで、依存関係を自由自在に選べるようになっている。
使用者側からすると組み込み易く有難いと思う反面、コードをややこしくしている原因でもある。
AppFactory::setContainer($container);
などすると、それが App 構築時のロジックに反映されていく。
2_Register route
ルーティングの登録。
Slim でどのように登録できるかは以下で把握しておいてください。
$app->post('/books', Book::class);
(第二引数は callable であればなんでも良い)
こういった登録はもちろん、
$app->group('/users/{id:[0-9]+}', function (RouteCollectorProxy $group) {
$group->get('/reset-password', ResetPassword::class);
$group->get('/verify-email', VerifyEmail::class);
...
グループ化もできる。
これは App
それ自身も RouteCollectorProxy
であり、それらは配下に RouteGroup
を付けられるからである。
class App extends RouteCollectorProxy implements RequestHandlerInterface
public function group(string $pattern, $callable): RouteGroupInterface
{
$pattern = $this->groupPattern . $pattern;
return $this->routeCollector->group($pattern, $callable);
}
public function group(string $pattern, $callable): RouteGroupInterface
{
$routeGroup = $this->createGroup($pattern, $callable);
$this->routeGroups[] = $routeGroup;
$routeGroup->collectRoutes();
array_pop($this->routeGroups);
return $routeGroup;
}
Middleware の追加もここで行う。
Middleware 自体の説明は 3a で行うので先にそちらを見ても良い。
App
にも、RouteGroup
にも、Route
にも追加できる。
$app->group('/foo', function (RouteCollectorProxy $group) {
$group->get('/bar', $callable)
->add(new RouteMiddleware());
})->add(new GroupMiddleware());
$app->add(new AppMiddleware());
Middleware は何にせよ、App
のプロパティ MiddlewareDispatcher
によって収集される。
(以下は App での実装。 RouteGroup 等でも最終的に $this->middlewareDispatcher->add($middleware);
している)
public function add($middleware): self
{
$this->middlewareDispatcher->add($middleware);
return $this;
}
Middleware についても、callable であればなんでも良い。Slim は懐が深い。
public function add($middleware): MiddlewareDispatcherInterface
{
if ($middleware instanceof MiddlewareInterface) {
return $this->addMiddleware($middleware);
}
if (is_string($middleware)) {
return $this->addDeferred($middleware);
}
if (is_callable($middleware)) {
return $this->addCallable($middleware);
}
...
この後、3bで Route()
について扱うのでここで触れておく。
new Route()
されるタイミングは、$app->post('hello', Hello::class);
などを実行したタイミング。App extends RouteCollectorProxy
なので、App::post
は以下となる。
public function post(string $pattern, $callable): RouteInterface
{
return $this->map(['POST'], $pattern, $callable);
}
$this->map()
は内部で $this->routeCollector->map()
を叩く。それが以下。
public function map(array $methods, string $pattern, $handler): RouteInterface
{
$route = $this->createRoute($methods, $pattern, $handler);
$this->routes[$route->getIdentifier()] = $route;
$routeName = $route->getName();
if ($routeName !== null && !isset($this->routesByName[$routeName])) {
$this->routesByName[$routeName] = $route;
}
$this->routeCounter++;
return $route;
}
/**
* @param string[] $methods
* @param callable|array{class-string, string}|string $callable
*/
protected function createRoute(array $methods, string $pattern, $callable): RouteInterface
{
return new Route(
$methods,
$pattern,
$callable,
$this->responseFactory,
$this->callableResolver,
$this->container,
$this->defaultInvocationStrategy,
$this->routeGroups,
$this->routeCounter
);
}
ここで new Route()
してRouteCollectorが保管している。
3_App::run(); Create ServerRequest
実際はライフサイクルはもう少し細かい。
$app->run()
内で、まずは PSR-7: HTTP message interfaces に準拠して ServerRequestInterface
(サーバーがハンドリングする用の HTTP Request Object)が作られる。
余談:この辺りも Glue Code によって非常に読みづらくなっている
public function run(?ServerRequestInterface $request = null): void
{
if (!$request) {
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
}
$response = $this->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter->emit($response);
}
デフォルトの Slim-Psr7
の実装であれば、最終的に ServerRequestFactory::createFromGlobals()
にて Slim\Psr7\Request implements ServerRequestInterface
オブジェクトが作成される。
ちなみに、デフォルトのSlim-Psr7
実装で使用されるクラス・メソッドは以下。参照されたし
class SlimPsr17Factory extends Psr17Factory
{
protected static string $responseFactoryClass = 'Slim\Psr7\Factory\ResponseFactory';
protected static string $streamFactoryClass = 'Slim\Psr7\Factory\StreamFactory';
protected static string $serverRequestCreatorClass = 'Slim\Psr7\Factory\ServerRequestFactory';
protected static string $serverRequestCreatorMethod = 'createFromGlobals';
}
個人的にこの辺りは低レイヤな HTTP 実装が多く、勉強になった
public static function createFromGlobals(): Request
{
/** @var string $method */
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri = (new UriFactory())->createFromGlobals($_SERVER);
$headers = Headers::createFromGlobals();
$cookies = Cookies::parseHeader($headers->getHeader('Cookie', []));
// Cache the php://input stream as it cannot be re-read
$cacheResource = fopen('php://temp', 'wb+');
$cache = $cacheResource ? new Stream($cacheResource) : null;
$body = (new StreamFactory())->createStreamFromFile('php://input', 'r', $cache);
$uploadedFiles = UploadedFile::createFromGlobals($_SERVER);
$request = new Request($method, $uri, $headers, $cookies, $_SERVER, $body, $uploadedFiles);
$contentTypes = $request->getHeader('Content-Type');
$parsedContentType = '';
foreach ($contentTypes as $contentType) {
$fragments = explode(';', $contentType);
$parsedContentType = current($fragments);
}
$contentTypesWithParsedBodies = ['application/x-www-form-urlencoded', 'multipart/form-data'];
if ($method === 'POST' && in_array($parsedContentType, $contentTypesWithParsedBodies)) {
return $request->withParsedBody($_POST);
}
return $request;
}
3a_Middleware
ここまできてようやく Middleware の話ができる。
Middleware は PSR-15: HTTP Server Request Handlers 準拠の実装であり、以下のドキュメントが非常にわかり易くまとまっている。
この同心円の実装は先ほども少し登場した MiddlewareDispatcher
周りのコードを追っていくとよくわかる。
まず、$app->run()
内で、Request
が作られた後に $this->middlewareDispatcher->handle()
が呼び出される。
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->tip->handle($request);
}
これだけ見ると ? なのだが、tip
とは Middleware stack の "先端" ということだ。それを念頭に addMiddleware を見ていただくと良い。
public function __construct(
RequestHandlerInterface $kernel,
?CallableResolverInterface $callableResolver = null,
?ContainerInterface $container = null
) {
$this->seedMiddlewareStack($kernel);
$this->callableResolver = $callableResolver;
$this->container = $container;
}
public function seedMiddlewareStack(RequestHandlerInterface $kernel): void
{
$this->tip = $kernel;
}
public function addMiddleware(MiddlewareInterface $middleware): MiddlewareDispatcherInterface
{
$next = $this->tip;
$this->tip = new class ($middleware, $next) implements RequestHandlerInterface {
private MiddlewareInterface $middleware;
private RequestHandlerInterface $next;
public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $next)
{
$this->middleware = $middleware;
$this->next = $next;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->next);
}
};
return $this;
}
$kernel
をスタックの一つ目の要素として、一番最後に追加(add($middleware)
)された Middleware が $tip に、その一つ前に追加された Middleware が $next に入れられていることがわかる。
Middleware は クラスの場合一般的に以下のような実装になっている。
class ContentLengthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// Add Content-Length header if not already added
$size = $response->getBody()->getSize();
if ($size !== null && !$response->hasHeader('Content-Length')) {
$response = $response->withHeader('Content-Length', (string) $size);
}
return $response;
}
}
上記のコード群より、
-
$this->tip->handle();
が呼び出されると、 -
$this->middleware->process($request, $this->next);
が呼び出される。 -
process
内では$handler->handle()
、つまり$this->next->handle();
が呼び出されるので、 - 次の
Middleware
に渡される、
という仕組みとなる。これが Middleware
の数だけ繰り返され、$this->next
が $kernel
までたどり着くと、関数のコールスタックを辿ってまた戻ってくる。
この流れが同心円状のアーキテクチャとして例示されているのだ。
では MiddlewareDispatcher::__construct()
の第一引数である $kernel
はどこからきたのか?というと、new App()
内の new RouteRunner()
である。 ここから
3b. Dispatch された Route の API Handler を実行する
へ移行するのだ。
public function __construct(
ResponseFactoryInterface $responseFactory,
?ContainerInterface $container = null,
?CallableResolverInterface $callableResolver = null,
?RouteCollectorInterface $routeCollector = null,
?RouteResolverInterface $routeResolver = null,
?MiddlewareDispatcherInterface $middlewareDispatcher = null
) {
parent::__construct(
$responseFactory,
$callableResolver ?? new CallableResolver($container),
$container,
$routeCollector
);
$this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector);
$routeRunner = new RouteRunner($this->routeResolver, $this->routeCollector->getRouteParser(), $this);
if (!$middlewareDispatcher) {
$middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $this->callableResolver, $container);
} else {
$middlewareDispatcher->seedMiddlewareStack($routeRunner);
}
$this->middlewareDispatcher = $middlewareDispatcher;
}
3b_RouteRunner
new RouteCollector
されるまでにも結構色々なオブジェクトがnew されるので以下に収集。
null だったら new、というように書かれているが、基本外部から依存関係を入れない限りはそのまま new されると思っておいて良い。
$this->routeCollector = $routeCollector ?? new RouteCollector($responseFactory, $callableResolver, $container);
$this->routeParser = $routeParser ?? new RouteParser($this);
$this->routeResolver = $routeResolver ?? new RouteResolver($this->routeCollector);
$routeRunner = new RouteRunner($this->routeResolver, $this->routeCollector->getRouteParser(), $this);
$kernel->handle() の実装を見る。
public function handle(ServerRequestInterface $request): ResponseInterface
{
// If routing hasn't been done, then do it now so we can dispatch
if ($request->getAttribute(RouteContext::ROUTING_RESULTS) === null) {
$routingMiddleware = new RoutingMiddleware($this->routeResolver, $this->routeParser);
$request = $routingMiddleware->performRouting($request);
}
if ($this->routeCollectorProxy !== null) {
$request = $request->withAttribute(
RouteContext::BASE_PATH,
$this->routeCollectorProxy->getBasePath()
);
}
/** @var Route<\Psr\Container\ContainerInterface|null> $route */
$route = $request->getAttribute(RouteContext::ROUTE);
return $route->run($request);
}
やってることは、
-
$routingMiddleware->performRouting();
でルーティング - 決定した
route->run();`route に対して、`
だけどここも複雑なんだよね
次の章から説明します。
3b-1_performRouting()
public function performRouting(ServerRequestInterface $request): ServerRequestInterface
{
$request = $request->withAttribute(RouteContext::ROUTE_PARSER, $this->routeParser);
$routingResults = $this->resolveRoutingResultsFromRequest($request);
...(略)
}
/**
* Resolves the route from the given request
*/
protected function resolveRoutingResultsFromRequest(ServerRequestInterface $request): RoutingResults
{
return $this->routeResolver->computeRoutingResults(
$request->getUri()->getPath(),
$request->getMethod()
);
}
public function computeRoutingResults(string $uri, string $method): RoutingResults
{
$uri = rawurldecode($uri);
if ($uri === '' || $uri[0] !== '/') {
$uri = '/' . $uri;
}
return $this->dispatcher->dispatch($method, $uri);
}
public function dispatch(string $method, string $uri): RoutingResults
{
$dispatcher = $this->createDispatcher();
$results = $dispatcher->dispatch($method, $uri);
return new RoutingResults($this, $method, $uri, $results[0], $results[1], $results[2]);
}
これ以下はデフォルトで nikic/FastRoute が使用される。ここだけで記事一つ書けてしまうので、以下の記事を紹介するに留める。
Blog post explaining how the implementation works and why it is fast.
とりあえず、$results
の中身はこうなっている
[FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']]
見つかったかどうか、どのhandlerを叩けばいいか、プレースホルダーで捕捉された変数は何か。
ここまできたらこの handler を引数付きで叩くだけですね!
$routingResults = $this->resolveRoutingResultsFromRequest($request);
で RouteResults
が帰ってくるので、その後を見ましょう。
public function performRouting(ServerRequestInterface $request): ServerRequestInterface
{
$request = $request->withAttribute(RouteContext::ROUTE_PARSER, $this->routeParser);
$routingResults = $this->resolveRoutingResultsFromRequest($request);
$routeStatus = $routingResults->getRouteStatus();
$request = $request->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults);
switch ($routeStatus) {
case RoutingResults::FOUND:
$routeArguments = $routingResults->getRouteArguments();
$routeIdentifier = $routingResults->getRouteIdentifier() ?? '';
$route = $this->routeResolver
->resolveRoute($routeIdentifier)
->prepare($routeArguments);
return $request->withAttribute(RouteContext::ROUTE, $route);
case RoutingResults::NOT_FOUND:
throw new HttpNotFoundException($request);
case RoutingResults::METHOD_NOT_ALLOWED:
$exception = new HttpMethodNotAllowedException($request);
$exception->setAllowedMethods($routingResults->getAllowedMethods());
throw $exception;
default:
throw new RuntimeException('An unexpected error occurred while performing routing.');
}
}
Found
の場合は、RouteCollector
からRouteResolver
が選び取ったRoute()
(2で触れましたね)に 引数がくっついてAttribute
に生やす感じですね。
その後、
- 決定した
route->run();` route に対して、`
これに進みます。
3b-2_route->run()
public function run(ServerRequestInterface $request): ResponseInterface
{
if (!$this->groupMiddlewareAppended) {
$this->appendGroupMiddlewareToRoute();
}
return $this->middlewareDispatcher->handle($request);
}
protected function appendGroupMiddlewareToRoute(): void
{
$inner = $this->middlewareDispatcher;
$this->middlewareDispatcher = new MiddlewareDispatcher($inner, $this->callableResolver, $this->container);
foreach (array_reverse($this->groups) as $group) {
$group->appendMiddlewareToDispatcher($this->middlewareDispatcher);
}
$this->groupMiddlewareAppended = true;
}
勘の鋭い方はここで気づくかもですが、また$this->middlewareDispatcher->handle()
されてますね。
別にMiddlewareDispatcherはシングルトンではないのがミソで、Route::__construct()
でnewされたものである。
$this->middlewareDispatcher = new MiddlewareDispatcher($this, $callableResolver, $container);
つまり、全体像を俯瞰すると、App()
全体の$kernel
であるRouteRunner()
の中で、Route()
がさらに$kernel
となり、RouteやRouteGroup専用のMiddlewareが先に$this->tip->handle()
されて〜という、3-aで話した流れに帰着するというわけだ。こうしないと入れ子構造つくれないよね。うーんよくできている。
よって、$this->middlewareDispatcher->handle()
内のコールスタックの終着点は Route::handle()
であることはお分かりだと思うのでそちらを見る。
public function handle(ServerRequestInterface $request): ResponseInterface
{
if ($this->callableResolver instanceof AdvancedCallableResolverInterface) {
$callable = $this->callableResolver->resolveRoute($this->callable);
} else {
$callable = $this->callableResolver->resolve($this->callable);
}
$strategy = $this->invocationStrategy;
$strategyImplements = class_implements($strategy);
if (
is_array($callable)
&& $callable[0] instanceof RequestHandlerInterface
&& !in_array(RequestHandlerInvocationStrategyInterface::class, $strategyImplements)
) {
$strategy = new RequestHandler();
}
$response = $this->responseFactory->createResponse();
return $strategy($callable, $request, $response, $this->arguments);
}
ついに、Responseという文字が...。
こちらも順を追って説明する。
resolveRoute() の中身
この段落は本筋から逸れるので見なくても良い。
final class CallableResolver implements AdvancedCallableResolverInterface
なので、$this->callableResolver->resolveRoute($this->callable);
を見れば良い。中身は、$this->resolveByPredicate($this->callable, [$this->callableResolver, 'isRoute'], 'handle');
で、さらにその先は
private function resolveByPredicate($toResolve, callable $predicate, string $defaultMethod): callable
{
$toResolve = $this->prepareToResolve($toResolve);
if (is_callable($toResolve)) {
return $this->bindToContainer($toResolve);
}
$resolved = $toResolve;
if ($predicate($toResolve)) {
$resolved = [$toResolve, $defaultMethod];
}
if (is_string($toResolve)) {
[$instance, $method] = $this->resolveSlimNotation($toResolve);
if ($method === null && $predicate($instance)) {
$method = $defaultMethod;
}
$resolved = [$instance, $method ?? '__invoke'];
}
$callable = $this->assertCallable($resolved, $toResolve);
return $this->bindToContainer($callable);
}
こんな感じで、まあ $callable
を本当に callable な形にして(ややこしい)、coutainer に bindTo している。$this->get('hoge')
とかを handler closure 内で行ったときに、$thisがcontainerを指したいがために、Slim3以前で主に使っていたからのようだ。ふーん。
ex.
$app->get('/hello/{name}', function ($request, $response, $args) {
$service = $this->get('MyService');
return $response->write($service->sayHello($args['name']));
});
strategy
ここで、strategyは Route ハンドラをどのような形で呼ぶかを決めている。
-
$handler($request, $response, $routeArgs)
→ 初期の$this->invocationStrategy := new RequestResponse()
-
$handler->handle($request)
→new RequestHandler()
後者の方が PSR-15 準拠なので、順当な実装であれば後者となるだろう。
それぞれのstrategyの中身も easy-to-understand なのでここに貼り付けておく。
public function __invoke(
callable $callable,
ServerRequestInterface $request,
ResponseInterface $response,
array $routeArguments
): ResponseInterface {
foreach ($routeArguments as $k => $v) {
$request = $request->withAttribute($k, $v);
}
/** @var ResponseInterface */
return $callable($request, $response, $routeArguments);
}
public function __invoke(
callable $callable,
ServerRequestInterface $request,
ResponseInterface $response,
array $routeArguments
): ResponseInterface {
if ($this->appendRouteArgumentsToRequestAttributes) {
foreach ($routeArguments as $k => $v) {
$request = $request->withAttribute($k, $v);
}
}
/** @var ResponseInterface */
return $callable($request);
}
Strategyが決まったら、さっくりとResponseの雛形を作って、
class ResponseFactory implements ResponseFactoryInterface
{
/**
* {@inheritdoc}
*/
public function createResponse(
int $code = StatusCodeInterface::STATUS_OK,
string $reasonPhrase = ''
): ResponseInterface {
$res = new Response($code);
if ($reasonPhrase !== '') {
$res = $res->withStatus($code, $reasonPhrase);
}
return $res;
}
}
route handler が実行される!
return $strategy($callable, $request, $response, $this->arguments);
いやーーーーー。長かった。
ここまでの流れが、ユーザーからすると AppFactory::create()
, $app->post()
, $app->add()
, $app->run()` のみに隠蔽されているのはおぞましいですね。
もうエンディングに突入したい気分をグッとこらえて、Responseへの道を進みましょうか。
3c_Reverse Middleware stack
ここまで完全に理解している方は飛ばしていただいていいと思います。
3aで言っていたことをそのまま書きます。
class ContentLengthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// Add Content-Length header if not already added
$size = $response->getBody()->getSize();
if ($size !== null && !$response->hasHeader('Content-Length')) {
$response = $response->withHeader('Content-Length', (string) $size);
}
return $response;
}
}
$this->tip->handle();
が呼び出されると、
$this->middleware->process($request, $this->next);
が呼び出される。
process 内では$handler->handle()
、つまり$this->next->handle();
が呼び出されるので、
次の Middleware に渡される、
という仕組みとなる。これが Middleware の数だけ繰り返され、$this->next
が $kernel までたどり着くと、関数のコールスタックを辿ってまた戻ってくる。
この流れが同心円状のアーキテクチャとして例示されているのだ。
あえて冗長に流れを可視化すると
1. App()->run()(App()->handle())
2. middleware2→process()
3. middleware1→process()
4. kernel->handle()
5. return
6. return
7. return
public function run(?ServerRequestInterface $request = null): void
{
if (!$request) {
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
}
$response = $this->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter->emit($response);
}
とすげー久しぶりにApp()->run() まで帰ってきたということだ。以上
3d_emit()
ということで、App()->run();
まで帰ってきました!めでたい!
App()->run()
を再掲しておく。
public function run(?ServerRequestInterface $request = null): void
{
if (!$request) {
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
}
$response = $this->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter->emit($response);
}
App()->handle()
から帰ってきた$response
をemitすれば長い旅路は終わりを告げる。
public function emit(ResponseInterface $response): void
{
$isEmpty = $this->isResponseEmpty($response);
if (headers_sent() === false) {
$this->emitHeaders($response);
// Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers.
// See https://github.com/slimphp/Slim/issues/1730
$this->emitStatusLine($response);
}
if (!$isEmpty) {
$this->emitBody($response);
}
}
ここはHTTP通信やファイル操作が絡むので、低レイヤなコードになっている。
public function isResponseEmpty(ResponseInterface $response): bool
{
if (in_array($response->getStatusCode(), [204, 205, 304], true)) {
return true;
}
$stream = $response->getBody();
$seekable = $stream->isSeekable();
if ($seekable) {
$stream->rewind();
}
return $seekable ? $stream->read(1) === '' : $stream->eof();
}
HTTP Status Code が
- 204 No Content
- 205 Reset Content
- 304 Not Modified
なら空のbodyとみなす。
もしくは、StreamInterface
である$response->getBody()
が空であればbodyは空である。
Response Body について
ここで補足。
レスポンスボディはSlim/Psr7
実装を使用している場合、Slim\Psr7\Stream
が使用される。
実態はphpがメモリ上に用意する一時ファイルである。 cf. https://qiita.com/mpyw/items/f24d3764fe3eedf132ff
/** class Slim\Psr7\Response */
$this->body = $body ?: (new StreamFactory())->createStream();
/** class Slim\Psr7\Factory\StreamFactory */
public function createStream(string $content = ''): StreamInterface
{
$resource = fopen('php://temp', 'rw+');
if (!is_resource($resource)) {
throw new RuntimeException('StreamFactory::createStream() could not open temporary file stream.');
}
fwrite($resource, $content);
rewind($resource);
return $this->createStreamFromResource($resource);
}
fopen / fwrite はなじみ深いかもしれないが
rewind / fseek などはなじみ浅いかもしれないので軽く説明。
php://tempという一時ファイル内のカーソルポインタを、
rewind: ファイルの先頭まで戻す
fseek: ファイルの特定の位置に移動する
というイメージ。seekableかどうかは、php://temp
はすでに seekable streamなので無視してOK。
本題に戻ると、次はheaders_sent()
で、ヘッダーが送出済みか判定している。
どうやってヘッダーが送出済みか判定しているの?と筆者も疑問に思ったが
PHPコアのSAPIの話になるのでここでは触れない。
php-srcの SAPI.c SAPI_API int sapi_send_headers(void)
あたりを見ると良いだろう。
headerをwriteする直前にSAPI_globalsのフラグをTRUEにしているようだ。
headerが未送出であることが確認できたら、header()
で実際に送出する(ために出力バッファに積む)。
private function emitHeaders(ResponseInterface $response): void
{
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
}
Set-Cookie のみ複数行の送出が許可されているのでその実装が入っている。
続けて、StatusLineの送出を行う。
private function emitStatusLine(ResponseInterface $response): void
{
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, replace: true, $response->getStatusCode());
}
Statusが最初じゃなくていいの?と思いますよね。
結論大丈夫で、header()
でOutput Buffer (出力バッファ、OB)に積んだ後、SAPI層でStatusをHeaderと分離するためです。
じゃあなんでこんな奇妙な実装なのかというと、
PHPの歴史的経緯により逆転させたそうです 😪
公式docsにも書いてます。
最後に、Bodyを送出します。
private function emitBody(ResponseInterface $response): void
{
$body = $response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
$amountToRead = (int) $response->getHeaderLine('Content-Length');
if (!$amountToRead) {
$amountToRead = $body->getSize();
}
if ($amountToRead) {
while ($amountToRead > 0 && !$body->eof()) {
$length = min($this->responseChunkSize, $amountToRead);
$data = $body->read($length);
echo $data;
$amountToRead -= strlen($data);
if (connection_status() !== CONNECTION_NORMAL) {
break;
}
}
} else {
while (!$body->eof()) {
echo $body->read($this->responseChunkSize);
if (connection_status() !== CONNECTION_NORMAL) {
break;
}
}
}
}
やってることはシンプルですね。Content-Length
分、echo
します。
ここにきて echo
が帰ってくるのはアツいですね。
StreamがEOFに到達すると、コードは終了します。
お疲れ様でした!!
終わりに
Slim の実装を一通りさらってみて、色々勉強になりました。
追いかけたのはSlimの標準実装を使った場合のみですが、他の繋ぎ込み部分もおおよそ似た実装になっているので全部理解できるのではないかと思います。
PHPの低レイヤ部分(特にHTTP/ファイル操作周り/PSR)への理解が深まりました。
次は PHP-DI / nikic\FastRoute / Slim\Psr7 どれにしようかな..