がるの健忘録 このページをアンテナに追加 RSSフィード

2018-06-24

[][][][]Slim docsの解析; The Response

https://www.slimframework.com/docs/v3/objects/response.html

ここも結構な大物だろう、と予想。


How to get the Response object

いやまぁHow to言われても「呼ばれる関数(メソッド)の第二引数でゲトれる」くらいだしなぁ。

<?php
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$app = new \Slim\App;
$app->get('/foo', function (ServerRequestInterface $request, ResponseInterface $response) {
    // Use the PSR 7 $response object

    return $response;
});
$app->run();

ここは一応突っ込みが無くもなくて。

「文字列をreturnすると、現状の$responseにその文字列をbodyとして書き込むような処理を追記してくれる」んだよね。

http://d.hatena.ne.jp/gallu/20180613/p1

        } elseif (is_string($newResponse)) {
            // if route callback returns a string, then append it to the response
            if ($response->getBody()->isWritable()) {
                $response->getBody()->write($newResponse);
            }
        }

あたりを参照。この辺が「そうそう簡単に変わる」ってのも考えにくいから「文字列returnしたらよしなにしてくれる」は、期待してよい動作、なんじゃないかなぁ? と思ってる。


The PSR 7 response object is injected into your Slim application middleware as the second argument of the middleware callable like this:

<?php
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

$app = new \Slim\App;
$app->add(function (ServerRequestInterface $request, ResponseInterface $response, callable $next) {
    // Use the PSR 7 $response object

    return $next($request, $response);
});
// Define app routes...
$app->run();

middleware周りはまた今度、まとめて徹底的に。なので、今回は一旦、パス。


The Response Status。割と大事ぽ。

$newResponse = $response->withStatus(302);

ここだけ把握しておけばよいんじゃないかなぁ………あぁ、一点気になること。

Response(以外もいくつかそうだけど)は、いわゆる「不変インスタンスである」とされているから、withStatusの実装って


vendor/slim/slim/Slim/Http/Response.php

    public function withStatus($code, $reasonPhrase = '')
    {
        $code = $this->filterStatus($code);

        if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) {
            throw new InvalidArgumentException('ReasonPhrase must be a string');
        }

        $clone = clone $this;
        $clone->status = $code;
        if ($reasonPhrase === '' && isset(static::$messages[$code])) {
            $reasonPhrase = static::$messages[$code];
        }

        if ($reasonPhrase === '') {
            throw new InvalidArgumentException('ReasonPhrase must be supplied for this code');
        }

        $clone->reasonPhrase = $reasonPhrase;

        return $clone;
    }

なんだよね。ポイントは「$clone = clone $this;」して「return $clone;」ってあたり。

なので、この辺でさっきの「文字列をreturn」と組み合わせると、なんか不一致が出そうな気が少しする、ので、気を付けたほうがよいかも。

………うん、returnは「ちゃんとResponseインスタンス返す」ようにしたほうが安全かも。

ちと、確認してみませう。


public/index.php

$app = new \Slim\App;
$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    //return 'test';
    return $response->getBody()->write('test');
});

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

GET /add_header HTTP/1.1

Host: example.com

HTTP/1.1 200 OK

Host: example.com

Date: Mon, 18 Jun 2018 05:07:09 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 4

testConnection closed by foreign host.

………あれ?

…………あ。

vendor/slim/slim/Slim/Http/Stream.php

    public function write($string)
    {
        if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) {
            throw new RuntimeException('Could not write to stream');
        }

        // reset size so that it will be recalculated on next call to getSize()
        $this->size = null;

        return $written;
    }

戻り値、stringか。


public/index.php

$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    $response->getBody()->write('test');
    //return 'test';
    return $response;
});

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

GET /add_header HTTP/1.1

Host: example.com

HTTP/1.1 304 Not Modified

Host: example.com

Date: Mon, 18 Jun 2018 05:18:11 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Connection closed by foreign host.

よし意図通り。

ってことは


$app->get('/add_header', function(Request $request, Response $response, array $args) {
    $response = $response->withStatus(304);
    //$response->getBody()->write('test');
    return 'test';
});

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

GET /add_header HTTP/1.1

Host: example.com

HTTP/1.1 200 OK

Host: example.com

Date: Mon, 18 Jun 2018 05:19:06 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 4

testConnection closed by foreign host.

だ〜よ〜ね〜〜。

これ、お作法的に

「最後は return $response; 」っての、徹底したほうが安全かも。ここだけは「Response、不変インスタンスじゃなければよかったのに」とか思う瞬間。まぁ、Slimがそう定義した訳でもない、から、ねぇ。

(実はお便利メソッドがresponseに生えてます。後述するwriteメソッド)。


The Response Headers。

$headers = $response->getHeaders();

とか、むしろ「デバッグ文脈」とか「解析の文脈」とか「テストの文脈」とか、で、便利そうな気がする。

便利ではあると思われるので、ほんのりと記憶しておきませう。


まぁあとは

$headerValueArray = $response->getHeader('Vary');
$headerValueString = $response->getHeaderLine('Vary');
if ($response->hasHeader('Vary')) {

とかあって

$newResponse = $oldResponse->withHeader('Content-type', 'application/json');
$newResponse = $oldResponse->withAddedHeader('Allow', 'PUT');

とかあるので、よき……なんだが、2点。

一つは「戻り値インスタンスである」って点。「不変インスタンスだったよねぇ」ってのを忘れると色々と切ない気がするので、注意。

もう一つは「withHeaderとwithAddedHeaderの違い」。

どちらも vendor/slim/slim/Slim/Http/Message.php に実装があって、かつ

    public function withAddedHeader($name, $value)
    {
        $clone = clone $this;
        $clone->headers->add($name, $value);

        return $clone;
    }

とかって実装なので、本当の実装は

vendor/slim/slim/Slim/Http/Headers.php

    public function add($key, $value)
    {
        $oldValues = $this->get($key, []);
        $newValues = is_array($value) ? $value : [$value];
        $this->set($key, array_merge($oldValues, array_values($newValues)));
    }
    public function set($key, $value)
    {
        if (!is_array($value)) {
            $value = [$value];
        }
        parent::set($this->normalizeKey($key), [
            'value' => $value,
            'originalKey' => $key
        ]);
    }

この辺。

「複数行」って割と(業務で付与するheader的には)珍しいと思うので、「基本は withHeader() 」って覚えておいて、そんなに問題ないんじゃないかなぁ。


あぁもちろん

Remove Header

$newResponse = $oldResponse->withoutHeader('Allow');

もそろってるので、適宜よしなに。

これも不変インスタンス以下略。


The Response Body については。

$body = $response->getBody();

でとれる……のは、bodyインスタンスデフォは vendor/slim/slim/Slim/Http/Body.php ここ。

なので

$response->getBody()->write();

この辺が、まぁよく使うところかなぁ、と。

上にも書いたけど「これの戻り値、string」なので、色々と注意、ではある。


Returning JSON

………あら、ナウい感じ。

vendor/slim/slim/Slim/Http/Response.php

    public function withJson($data, $status = null, $encodingOptions = 0)
    {
        $response = $this->withBody(new Body(fopen('php://temp', 'r+')));
        $response->body->write($json = json_encode($data, $encodingOptions));

        // Ensure that the json encoding passed successfully
        if ($json === false) {
            throw new \RuntimeException(json_last_error_msg(), json_last_error());
        }

        $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8');
        if (isset($status)) {
            return $responseWithJson->withStatus($status);
        }
        return $responseWithJson;
    }

便利〜。

………こっちは「return $response->withJson($data);」ってやってもいいのか………微妙に面倒w

まぁ「withついてるからcloneインスタンスが返ってくる」って覚えてもいいんだけどねぇ。

とはいえいずれにしても、json returnが考慮されているのは、よいこった。


……で、何気なくみていたら

vendor/slim/slim/Slim/Http/Response.php

    public function write($data)
    {
        $this->getBody()->write($data);

        return $this;
    }

やだぁ便利なのあるじゃないですか。

ってことは

return $response->write(HTML文字列);

でいけますなぁ。

こっちこっち。使うの、こっち。


Returning a Redirect。

うん、これも大事。

return $response->withRedirect('/new-url', 301);

シンプル。

………Slimって、ルーティングに「名前付き」ってできたんだっけか?

https://www.slimframework.com/docs/v3/objects/router.html#route-names

に出てくるっぽいので、次回に回そうw


いじょ。

割とシンプルだったなぁ………そういや、Cookie周りのことはなんも書いてないw

まぁ、一通り掘り込んだ上で「なかったら」改めて考察しませう。

2018-06-20

[][][][]Slim docsの解析; The Request

https://www.slimframework.com/docs/v3/objects/request.html

多分、ここは大物w


とりあえずルーティング設定。

関数とか「設定できる」んだろうけどやる気はないんで、無視w


The Request Method

………まぁ一通り。GET,PUT,POST,DELETEくらいしか使う気ないしなぁ。


で、メソッド指定2種。

You can include a _METHOD parameter in a POST request’s body. The HTTP request must use the application/x-www-form-urlencoded content type.

POST /path HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 22

data=value&_METHOD=PUT

You can also override the HTTP request method with a custom X-Http-Method-Override HTTP request header. This works with any HTTP request content type.

POST /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length: 16
X-Http-Method-Override: PUT

{"data":"value"}

ふむ……この辺、お他所はどうだっただろさね??

ちぃと実験コード書いてみるかねぇ。

実験目的としては

そもそも、ちゃんと「PUT」とかのメソッドでcallした時はどーゆールーティングになるのか

・上述のような「メソッドと上書きメソッド」の時の動き

あたり。


まずは、壮絶に雑なコードを一筆。

public/index.php

$app = new \Slim\App;
$app->get('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, get\n");
    return $response;
});
$app->post('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, post\n");
    return $response;
});
$app->put('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, put\n");
    return $response;
});
$app->delete('/', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, delete\n");
    return $response;
});

んで、おおざっぱにcall。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

GET / HTTP/1.1

Host: example.com

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:14:49 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 11

Hello, get

Connection closed by foreign host.

よし。


では、PUTで試し。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

PUT / HTTP/1.1

Host: example.com

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:15:49 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 11

Hello, put

Connection closed by foreign host.

ふむりやっぱり無問題

一応、DELETEも。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

DELETE / HTTP/1.1

Host: example.com

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:16:16 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 14

Hello, delete

Connection closed by foreign host.

OK


で、これに「糅てて加えて」別フォーマットがあるのか。

ちと修正して。

$app = new \Slim\App;
$app->get('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, get\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->post('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, post\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->put('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, put\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});
$app->delete('/path', function(Request $request, Response $response, array $args) {
    $response->getBody()->write("Hello, delete\n" . 'getMethod: ' . $request->getMethod() . "\n" . 'getOriginalMethod: ' . $request->getOriginalMethod() . "\n");
    return $response;
});

サンプルに出ているのを、そのまま。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

POST /path HTTP/1.1

Host: example.com

Content-type: application/x-www-form-urlencoded

Content-length: 22

data=value&_METHOD=PUT

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:20:52 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 50

Hello, put

getMethod: PUT

getOriginalMethod: POST

Connection closed by foreign host.


$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

POST /path HTTP/1.1

Host: example.com

Content-type: application/json

Content-length: 16

X-Http-Method-Override: PUT

{"data":"value"}

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:21:29 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 50

Hello, put

getMethod: PUT

getOriginalMethod: POST

Connection closed by foreign host.

ふむ面白い。

個人的にはX-Http-Method-Overrideヘッダのほうが好きかなぁ、なんとなく。


………ちと、好奇心。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

POST /path HTTP/1.1

Host: example.com

Content-type: application/x-www-form-urlencoded

Content-length: 22

X-Http-Method-Override: DELETE

data=value&_METHOD=PUT

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:23:29 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 56

Hello, delete

getMethod: DELETE

getOriginalMethod: POST

Connection closed by foreign host.

ヘッダのほうが強いのかしらん?

まぁあんまりない事象だとは思うんだけど、後々用に、軽く実験。


お次はThe Request URI

The PSR 7 Request object’s URI is itself an object that provides the following methods to inspect the HTTP request’s URL parts:

    getScheme()
    getAuthority()
    getUserInfo()
    getHost()
    getPort()
    getPath()
    getBasePath()
    getQuery() (returns the full query string, e.g. a=1&b=2)
    getFragment()
    getBaseUrl()

へぇ。色々あるのか。


You can get the query parameters as an associative array on the Request object using getQueryParams().

これは多分使わないかなぁ。


Base Path

If your Slim application's front-controller lives in a physical subdirectory beneath your document root directory, you can fetch the HTTP request's physical base path (relative to the document root) with the Uri object's getBasePath() method. This will be an empty string if the Slim application is installed in the document root's top-most directory.

あ、解析で出てきたBase Path

「Slimアプリケーションのフロントコントローラがドキュメントルートディレクトリの下の物理サブディレクトリにある場合、UriオブジェクトのgetBasePath()メソッドを使用して、HTTPリクエストの物理ベースパス(ドキュメントルートからの相対パス)を取得できます。(機械翻訳)」なるほどぉ。

Cookieとかの設定用途とか、で、使いそうだなぁ。

ちと、どこかで必要な時にでも、がっつりやってみよう。


The Request Headers

うんまぁ。

$headers = $request->getHeaders();
foreach ($headers as $name => $values) {
    echo $name . ": " . implode(", ", $values);
}

で、とれるぽ。

あとは

$headerValueArray = $request->getHeader('Accept');
$headerValueString = $request->getHeaderLine('Accept');
if ($request->hasHeader('Accept')) {
    // Do something
}

など。


The Request Body

あ、ちょうど気になってたやつ。

$parsedBody = $request->getParsedBody();

public/index.php

$app->put('/path', function(Request $request, Response $response, array $args) {
    ob_start();
    var_dump( $request->getParsedBody() );
    $s = ob_get_clean();

    $response->getBody()->write("Hello, put\n" . $s . "\n");
    return $response;
});

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

PUT /path HTTP/1.1

Host: example.com

Content-type: application/x-www-form-urlencoded

Content-length:21

data=value&data2=test

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:45:17 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 91

Hello, put

array(2) {

["data"]=>

string(5) "value"

["data2"]=>

string(4) "test"

}

Connection closed by foreign host.


$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

PUT /path HTTP/1.1

Host: example.com

Content-type: application/json

Content-length:31

{"data":"value", "data2": 10}

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:44:25 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 82

Hello, put

array(2) {

["data"]=>

string(5) "value"

["data2"]=>

int(10)

}

Connection closed by foreign host.

こっちは綺麗に。


………ふむ。

$ telnet localhost 8080

Trying ::1...

telnet: connect to address ::1: Connection refused

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

PUT /path HTTP/1.1

Host: example.com

Content-length:31

{"data":"value", "data2": 10}

HTTP/1.1 200 OK

Host: example.com

Date: Fri, 15 Jun 2018 08:46:09 +0000

Connection: close

X-Powered-By: PHP/7.2.6

Content-Type: text/html; charset=UTF-8

Content-Length: 17

Hello, put

NULL

Connection closed by foreign host.

あぁ。Content-type見てるのか。うん、綺麗だ。


さて………

$body = $request->getBody();

ってのもあるが……なんかこってりしそうだなぁ。

$app->put('/path', function(Request $request, Response $response, array $args) {
    ob_start();
    //var_dump( $request->getParsedBody() );
    var_dump( $request->getBody() );
    $s = ob_get_clean();

    $response->getBody()->write("Hello, put\n" . $s . "\n");
    return $response;
});
$ telnet localhost 8080
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
PUT /path HTTP/1.1
Host: example.com
Content-type: application/json
Content-length:31

{"data":"value", "data2": 10}
HTTP/1.1 200 OK
Host: example.com
Date: Fri, 15 Jun 2018 08:47:56 +0000
Connection: close
X-Powered-By: PHP/7.2.6
Content-Type: text/html; charset=UTF-8
Content-Length: 304

Hello, put
object(Slim\Http\RequestBody)#50 (7) {
  ["stream":protected]=>
  resource(46) of type (stream)
  ["meta":protected]=>
  NULL
  ["readable":protected]=>
  NULL
  ["writable":protected]=>
  NULL
  ["seekable":protected]=>
  NULL
  ["size":protected]=>
  NULL
  ["isPipe":protected]=>
  NULL
}

Connection closed by foreign host.

あ。思ったより穏当。

細かいところはまた今度、実装する時に見てみましょ。


同様に後回ししたいのがUploaded Files。

$files = $request->getUploadedFiles();

で「Each object in the $files array is a instance of \Psr\Http\Message\UploadedFileInterface 以下略」なので、「1ファイルが1インスタンス」って単位だろうから、さほど困ることもなさそうだ。


お次、Request Helpers。

いわゆる「お便利さん」ぽ。


Detect XHR requests………XMLHttpRequestとか、久しく耳にも目にもしておりませぬなぁ。割愛w


Content Type………まぁ欲しい、時も。

$contentType = $request->getContentType();

Media Type………お外から入ってくるこれは全く信用してないw、ので、割愛。


Character Set、Content Length。うんまぁ欲しい時はほしいだろうさね。

$charset = $request->getContentCharset();
$length = $request->getContentLength();

Request Parameter。

To fetch single request parameter value, use methods: getParam(), getQueryParam(), getParsedBodyParam(), getCookieParam(), getServerParam(), counterparts of PSR-7’s plural form get*Params() methods.

「単体の値取得」ならこれ、ですか。入力で一番使いそうな予感。

ざっくりいくんなら getParam() かしらん? とも思うんだが、ざっくり過ぎる気もする。

    public function getParam($key, $default = null)
    {
        $postParams = $this->getParsedBody();
        $getParams = $this->getQueryParams();
        $result = $default;
        if (is_array($postParams) && isset($postParams[$key])) {
            $result = $postParams[$key];
        } elseif (is_object($postParams) && property_exists($postParams, $key)) {
            $result = $postParams->$key;
        } elseif (isset($getParams[$key])) {
            $result = $getParams[$key];
        }

        return $result;
    }

あぁ「$_POSTまたは$_GET」的な感じ、なのか。かつ、$_POST的なほうが優先度高め。

これなら、あり、かなぁ。


Route Object

Sometimes in middleware you require the parameter of your route.

む……middlewareのお作法そもそも理解していない。

add()、が、そうなのかし??

あとで、具体的には「Routing」のところで #route-middleware で項目があるから、その時にさかのぼってみませうか。


Media Type Parsers

ん……「中の動き」っぽいなぁ。一旦省略。


Attributes

あ、よぉ分からん所だった一つ。

$app->add(function ($request, $response, $next) {
    $request = $request->withAttribute('session', $_SESSION); //add the session storage to your request as [READ-ONLY]
    return $next($request, $response);
});
$app->get('/test', function ($request, $response, $args) {
    $session = $request->getAttribute('session'); //get the session from the request

    return $response->write('Yay, ' . $session['name']);
});

あぁ。

MagicWeaponだと「bag」とか言ってるあたりのやつか。「持ちまわる諸々」。

requestは「不変インスタンス」だから、withAttributeは「内部でcloneしている」んだよね、的な。なので「戻り値を受け取って、そっちを$nextに渡してる」。

この辺もmiddlewareかな。


……そういえば、セッション的なのって、どうなってるんだべさね?

その辺も少し意識に入れておきませう。


ってなわけでRequest終了。

やっぱり長かったw

2018-06-19

[][][][]Slim docsの解析; The Application

https://www.slimframework.com/docs/

の中から「The Application」をざっくりと見て、興味深いところをメモ。


https://www.slimframework.com/docs/v3/objects/application.html

$config = [
    'settings' => [
        'displayErrorDetails' => true,

        'logger' => [
            'name' => 'slim-app',
            'level' => Monolog\Logger::DEBUG,
            'path' => __DIR__ . '/../logs/app.log',
        ],
    ],
];
$app = new \Slim\App($config);

settingはスケルトンで見たので知ってる〜。


ただ

Updating Settings

$settings = $container->get('settings');
$settings->replace([
        'displayErrorDetails' => true,
        'determineRouteBeforeAppMiddleware' => true,
        'debug' => true
    ]);
]);

は、なるほどふむり、興味深い。

……ただ、ってことは「Slimのデフォの値を把握してなきゃなんない」よねぇって思ったら Slim Default Settings がある、ので、ざっくりと備忘録的に。


httpVersion

まぁそのまんまだよねぇ。デフォ1.1。いつか2.0になるのかねぇ???


responseChunkSize

いわゆる「送りつける1塊のサイズ」。この辺、あんまりPHPだと意識していないような気がするんだけど、意識するとなにか変わるのかしらん?

デフォは4096バイト。


outputBuffering

出力バッファリングの有無。

fasleで「バッファリングしない」、appendで「後ろにつける」、prependで「前につける」、のいずれか。……それ以外の値の時ってどうなるんだろうねぇ?

デフォはappend。まぁデフォでいいと思うなぁ。

単純なtrue(出力バッファリングだけする)とか欲しいような気もしたけど、それ、appendになるわ。


determineRouteBeforeAppMiddleware

長いw

「trueの場合、ミドルウェアが実行される前にルートが計算されます。(機械翻訳)」………ふむ、なんかグっと来ない感じ。

デフォはfalseなので、あんまり使わないかしらん?


displayErrorDetails

「trueの場合、例外に関する追加情報がデフォルトのエラーハンドラによって表示されます。(機械翻訳)」で、デフォがfalse。

ふむ……開発環境でtrueにしておくと旨味があるのかしらん?


addContentLengthHeader

Content-Length付与の有無。デフォはtrueだし、まぁここはtrueのままでいいんじゃないかなぁ?


routerCacheFile

ルートファイルのキャッシュの有無で、デフォルトはfalse(キャッシュしない)。

……キャッシュの仕組みがよっぽどわかればともかく、それ以外だと、あんまりグっと来ないかなぁ。


設定したくなりそうなのはdisplayErrorDetailsくらいかしらん?

………あれ?

上にある「debug」ってなんじゃらほい?

grepしてみたけど、グっとくる情報がないなぁ。slim_test_plain(Slimしか入れてない環境)だと、引っかかってこないし。

とりあえず「一旦、デフォでは存在しない」って認識でOKかしらん。


あと。

$settings->replace()、スケルトンのほうだと使ってないのだよねぇ……軽く確認。

    public function __construct(array $values = [])
    {
        parent::__construct($values);

        $userSettings = isset($values['settings']) ? $values['settings'] : [];
        $this->registerDefaultServices($userSettings);
    }
    private function registerDefaultServices($userSettings)
    {
        $defaultSettings = $this->defaultSettings;

        /**
         * This service MUST return an array or an
         * instance of \ArrayAccess.
         *
         * @return array|\ArrayAccess
         */
        $this['settings'] = function () use ($userSettings, $defaultSettings) {
            return new Collection(array_merge($defaultSettings, $userSettings));
        };

        $defaultProvider = new DefaultServicesProvider();
        $defaultProvider->register($this);
    }

ふむ、array_mergeがポイントか。

array_mergeは後だし有効だったはずだから………userSettingsが上書き。

なので「明示的にコンストラクタに渡して設定したらそっちが有効になる」なぁ。

ってことは、index.php付近での「初期設定」の時は「値を渡しておけば問答無用でそっち」になるぽ。

$settings->replace()は、とりあえず「使わない」かなぁ。この状態だと。


割とシンプルに調査終了。

2018-06-16

[][][]Slim skeletonの解析

さて続きましては slim/slim-skeleton の解析。

https://github.com/slimphp/Slim-Skeleton なので、なんとなし「多分公式が推奨してるんじゃないかと思われる方向性」が確認できるんじゃないかなぁ、と期待。

……で調べたら、別の人が作ったスケルトンもあるのね( https://github.com/oanhnn/slim-skeleton とか)。

ふぅんこれはこれで面白いなぁ、と思ったんだが、まぁいったんは「公式の推奨方向」の確認から。


まずは、起点から確認をしてみませう。

public/index.php

if (PHP_SAPI == 'cli-server') {
    // To help the built-in PHP dev server, check if the request was actually for
    // something which should probably be served as a static file
    $url  = parse_url($_SERVER['REQUEST_URI']);
    $file = __DIR__ . $url['path'];
    if (is_file($file)) {
        return false;
    }
}

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

session_start();

// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);

// Set up dependencies
require __DIR__ . '/../src/dependencies.php';

// Register middleware
require __DIR__ . '/../src/middleware.php';

// Register routes
require __DIR__ . '/../src/routes.php';

// Run app
$app->run();

前回は(一応公式に書かれたものをベースにしてるとはいえ)手で作ったのが、今回はあらかじめ入っているという素晴らしさ。

さて、細かく見ていきませう。


if (PHP_SAPI == 'cli-server') {
    // To help the built-in PHP dev server, check if the request was actually for
    // something which should probably be served as a static file
    $url  = parse_url($_SERVER['REQUEST_URI']);
    $file = __DIR__ . $url['path'];
    if (is_file($file)) {
        return false;
    }
}

へぇブロックしたいんだ。

「なぜブロックしたいのか」が微妙に不明なんだけど、まぁいったん。

「ビルトインのPHP devサーバを手助けするために、リクエストが実際には静的ファイルとして提供されるべきものであるかどうかを確認してください(機械翻訳)」って書いてあるので、まぁ、なんか 必要なんでしょう、程度にざっくりと。

いや「is_fileのif文の中がfalse returnなのが不思議だなぁ」程度の感触。


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

いわゆる「一般的なオートローダーの取り込み」。これは前述の時もあったのであんまり気にならず。


session_start();

へぇセッション使うようにしてるのか。

……うん確かに、plainなのだとセッション使ってなかったような記憶がかすかにあるなぁ。

でもこれ「セッション保存をテーブルにしたいとき」の処理とか、どうしよ?

プレファイル作る? おとなしくindex.phpに直書き?


// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);

Appに引数渡してる。

settings.phpって「配列を返す」んだよね。


src/settings.php

<?php
return [
    'settings' => [
        'displayErrorDetails' => true, // set to false in production
        'addContentLengthHeader' => false, // Allow the web server to send the content-length header

        // Renderer settings
        'renderer' => [
            'template_path' => __DIR__ . '/../templates/',
        ],

        // Monolog settings
        'logger' => [
            'name' => 'slim-app',
            'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
            'level' => \Monolog\Logger::DEBUG,
        ],
    ],
];

初手のkey「setting」は決まり文句かしらん。変えてもいいけど後々面倒だし、変えないほうが楽だよねぇ、的な。

…… config() 関数とか作って楽をしてもよいかも。

あと、自動でMonolog入れてる。

displayErrorDetails と addContentLengthHeader は「Slimがもともと使うことを想定していた変数」だねぇ。……今度その辺も整理してみようかしらん?


まぁいずれにしても「いろいろな設定情報を取り込んでる」のが見て取れるげ。

「環境差異のある情報」とかも、うまいことここに入れ込みたいなぁ。


次。

// Set up dependencies
require __DIR__ . '/../src/dependencies.php';

src/dependencies.php

$container = $app->getContainer();

// view renderer
$container['renderer'] = function ($c) {
    $settings = $c->get('settings')['renderer'];
    return new Slim\Views\PhpRenderer($settings['template_path']);
};

// monolog
$container['logger'] = function ($c) {
    $settings = $c->get('settings')['logger'];
    $logger = new Monolog\Logger($settings['name']);
    $logger->pushProcessor(new Monolog\Processor\UidProcessor());
    $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
    return $logger;
};

containerに入れるやつだ。

PhpRendererって知らないなぁ……Zend2に入ってるやつかな??

この辺は差し替えたいかも。まぁ差し替えるのは極めて簡単でせう。


// Register middleware
require __DIR__ . '/../src/middleware.php';

src/middleware.php

<?php
// Application middleware

// e.g: $app->add(new \Slim\Csrf\Guard);

多分ってか確実に「ミドルウェアの設定を書くところ」。これが「実処理を書くところ」だったらびっくりだお(笑

ミドルウェアは、まだ、全然触ってないからなぁ。

ミドルウェア自体をちゃんと扱い始めてから、改めて考えてみませう。


// Register routes
require __DIR__ . '/../src/routes.php';

src/routes.php

use Slim\Http\Request;
use Slim\Http\Response;

// Routes

$app->get('/[{name}]', function (Request $request, Response $response, array $args) {
    // Sample log message
    $this->logger->info("Slim-Skeleton '/' route");

    // Render index view
    return $this->renderer->render($response, 'index.phtml', $args);
});

ルーティング書くところ。

まぁそのまんま、だねぇ。

戻り値が、おそらくはこれ「string型」のreturnだなぁ。まぁわかりやすくてよいと思う。


で、あとは

// Run app
$app->run();

は同じ。


あとはphpunit.xmlが入っていて tests/Functional があるあたりが興味深いかな。

ちと、軽く潜ってみませう。


まずはphpunit.xml

<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="SlimSkeleton">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

うんまぁなんていうか「これ以上ないってくらいスタンダード」なパターンだと思うんだがどうだろw

testsディレクトリって、大体「この名前」だよねぇ。それ以外のってあんまり見たことがない。

さて。testsん中にはFunctionalってディレクトリがあって。

BaseTestCase.phpが「継承元クラス」っぽいので、先に片付きそうなHomepageTest.phpを軽く触ってからBaseTestCase.phpにいきませう。


HomepageTest.php

    public function testGetHomepageWithoutName()
    {
        $response = $this->runApp('GET', '/');

        $this->assertEquals(200, $response->getStatusCode());
        $this->assertContains('SlimFramework', (string)$response->getBody());
        $this->assertNotContains('Hello', (string)$response->getBody());
    }

メソッドの一例。

「testから始まっているよねぇ」とかいうベーシックなところはおいといて。

「$this->runApp」とかが多分「このメソッドでこのURIを叩いてみる」的な動きなんだねぇ。………へぇ便利だ。

assertContainsとかあんまり使わないから記憶にないなぁ………なるほど「この文字が含まれる」か。

assertNotContainsは逆で「含まれないこと」と。

とりあえず「$this->assertEquals(200, $response->getStatusCode());」を全Pageぶん回すだけでも、色々と楽なんじゃなかろうか? 認証系とかあるんだろうけどさ。


では、速やかにBaseTestCase.php

直近はrunApp()と、あとは初期処理系メソッドの確認かなぁ。


BaseTestCase.php

class BaseTestCase extends \PHPUnit_Framework_TestCase
{

「だ〜よ〜ね〜〜」なあたりの確認からstart。


………ふむ、__constructはなし、か。

PHPUnit、たしか

setUpBeforeClass() // 一回だけ
loop {
	setUp()
		テストメソッド本体
	tearDown()
} pool
tearDownAfterClass() // 一回だけ

こーゆー構造だったよねぇ。


setUpBeforeClass()は……なし。

setUp()……も、なし、か。したら直接runApp()いきますか。

……っつかこのクラス、runApp()のみだわ。


    public function runApp($requestMethod, $requestUri, $requestData = null)
    {
        // Create a mock environment for testing with
        $environment = Environment::mock(
            [
                'REQUEST_METHOD' => $requestMethod,
                'REQUEST_URI' => $requestUri
            ]
        );

引数は一旦、おいといて。

Environment::mock、ですか。


vendor/slim/slim/Slim/Http/Environment.php を見てみる……あぁCollectionの継承先クラス、なのか。

ようは「データをためておく」程度の感じ。

んで

        // Set up a request object based on the environment
        $request = Request::createFromEnvironment($environment);

か。

    public static function createFromEnvironment(Environment $environment)
    {

Environmentが引数の前提だし、完全に「これ専用のメソッド」だねぇ。


        // Set up a response object
        $response = new Response();

これは、まぁ、うん。


        // Use the application settings
        $settings = require __DIR__ . '/../../src/settings.php';

        // Instantiate the application
        $app = new App($settings);

        // Set up dependencies
        require __DIR__ . '/../../src/dependencies.php';

        // Register middleware
        if ($this->withMiddleware) {
            require __DIR__ . '/../../src/middleware.php';
        }

        // Register routes
        require __DIR__ . '/../../src/routes.php';

この辺も、まぁ、うん。

ただ、ここ「共通化」できねぇかなぁ?


        // Process the application
        $response = $app->process($request, $response);

process()………あぁrun()で呼ばれる所の「実処理担当」のあたりだ。ふむふむ。

んで

        // Return the response
        return $response;

か、なるほど。


たしかに「URIを渡して、実行部分だけ切り抜いてる」感じだなぁ。

これなら、多分

・Content-typeがjsonかどうか

jsonのフォーマットの確認

とかもできそうな気がするので、ざっくりしたテストくらいなら、一通りかけそうだ。


なお、tearDown()もtearDownAfterClass()も見つかりませんでしたまる。


細かい話をすると「なんで Functional ?」とか思うんだが、多分これはおいちゃんの英語能力の欠落が原因だろうなぁ、的な。

………あぁ、そうか。例えばCodeceptionなんかでも「機能テスト」ってあるし、URI見ると FunctionalTests だから、これでよいのか。

そうすると……同じレベルで「Unit」とか作るべきかなぁ。納品的には「Acceptance」も大事だろうし。

まぁ、その辺もおいおい、だねぇ。


ふむ………

なんとなく「もう少しいろいろと付け足したりしたい」感じではあるけど、確かに、ベースにはできそうな気がする。

その辺の「付け足したいところ」は、実際に書きながら、少しづつ考えていきませうか。

2018-06-13

[][][]解析その4

核心……、の、はず!!w


「Routeの__invoke」からのstartでございます。

vendor/slim/slim/Slim/Route.php

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        $this->callable = $this->resolveCallable($this->callable);

        /** @var InvocationStrategyInterface $handler */
        $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse();

        $newResponse = $handler($this->callable, $request, $response, $this->arguments);

        if ($newResponse instanceof ResponseInterface) {
            // if route callback returns a ResponseInterface, then use it
            $response = $newResponse;
        } elseif (is_string($newResponse)) {
            // if route callback returns a string, then append it to the response
            if ($response->getBody()->isWritable()) {
                $response->getBody()->write($newResponse);
            }
        }

        return $response;
    }

面白そうなんで、ちと細かく見ていきますか。


       $this->callable = $this->resolveCallable($this->callable);

resolveCallableメソッド………ないし。

    use MiddlewareAwareTrait;

からあたりつけて vendor/slim/slim/Slim/MiddlewareAwareTrait.php ………いねぇ。

class Route extends Routable implements RouteInterface

だから継承元クラス、かな? vendor/slim/slim/Slim/Routable.php ………いねぇ。

Routableに

    use CallableResolverAwareTrait;

なので

vendor/slim/slim/Slim/CallableResolverAwareTrait.php

    /**
     * Resolve a string of the format 'class:method' into a closure that the
     * router can dispatch.
     *
     * @param callable|string $callable
     *
     * @return \Closure
     *
     * @throws RuntimeException If the string cannot be resolved as a callable
     */
    protected function resolveCallable($callable)
    {
        if (!$this->container instanceof ContainerInterface) {
            return $callable;
        }

        /** @var CallableResolverInterface $resolver */
        $resolver = $this->container->get('callableResolver');

        return $resolver->resolve($callable);
    }

いたいた。

「Resolve a string of the format 'class:method' into a closure that the router can dispatch.」あ、ここか。いきなり「見つけたいものの一つ」を発見。

callableResolver が、処理の箇所的に怪しいなぁ。


念のために確認……

vendor/slim/slim/Slim/DefaultServicesProvider.php

            $container['callableResolver'] = function ($container) {
                return new CallableResolver($container);
            };

うんクラス名そのまんま。

vendor/slim/slim/Slim/CallableResolver.php

    public function resolve($toResolve)
    {
        if (is_callable($toResolve)) {
            return $toResolve;
        }

        if (!is_string($toResolve)) {
            $this->assertCallable($toResolve);
        }

        // check for slim callable as "class:method"
        if (preg_match(self::CALLABLE_PATTERN, $toResolve, $matches)) {
            $resolved = $this->resolveCallable($matches[1], $matches[2]);
            $this->assertCallable($resolved);

            return $resolved;
        }

        $resolved = $this->resolveCallable($toResolve);
        $this->assertCallable($resolved);

        return $resolved;
    }

はいジャストミート

・呼べる形式(is_callable)ならそのまま

class:methodなら「resolveCallable」してから「assertCallable」

ふむ……resolveCallableの中身から、かなぁ。

    protected function resolveCallable($class, $method = '__invoke')
    {
        if ($this->container->has($class)) {
            return [$this->container->get($class), $method];
        }

        if (!class_exists($class)) {
            throw new RuntimeException(sprintf('Callable %s does not exist', $class));
        }

        return [new $class($this->container), $method];
    }

あら。「コンテナにあるならそのクラスを使う」なんだ。めったに引っかからないとは思うんだけど「ものすごくレアに"同一インスタンス"であるためにはまる」とか、ありそうな気がするなぁ………もわっとした、漠然としたイメージだけど。ちょっとだけ気にしておこう。

あと、コンストラクタにcontainer渡してるのか。ふむり。

で、戻り値は「インスタンスメソッド名」の配列、か。

デフォルト引数の「$method = '__invoke'」も、ちょっとおもしろいなぁ……これに依存するコード書くと、微妙にトリッキーになりそうだけど。


お次、assertCallable。

    protected function assertCallable($callable)
    {
        if (!is_callable($callable)) {
            throw new RuntimeException(sprintf(
                '%s is not resolvable',
                is_array($callable) || is_object($callable) ? json_encode($callable) : $callable
            ));
        }
    }

あぁ。assert、だからまんま、か。

ってことは、これで「インスタンスメソッド名の配列」がreturnされるんだな。

なので


vendor/slim/slim/Slim/Route.php

       $this->callable = $this->resolveCallable($this->callable);

の$this->callableには「インスタンスメソッド名、の配列」が入ってくる、と。


        /** @var InvocationStrategyInterface $handler */
        $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse();

foundHandler………なかったっけ?

vendor/slim/slim/Slim/DefaultServicesProvider.php

            $container['foundHandler'] = function () {
                return new RequestResponse;
            };

あったあった。

んじゃ、RequestResponseを閲覧。

………あら? 直下にいない。findコマンドで探してみる。

$ find ./ -name RequestResponse.php

./vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php

あらためて

vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php

    public function __invoke(
        callable $callable,
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $routeArguments
    ) {
        foreach ($routeArguments as $k => $v) {
            $request = $request->withAttribute($k, $v);
        }

        return call_user_func($callable, $request, $response, $routeArguments);
    }

「call_user_func」うんだよねぇ。

微妙に

        foreach ($routeArguments as $k => $v) {
            $request = $request->withAttribute($k, $v);
        }

の旨味が見えないんだけど、まぁとりあえず「requestのAttributeの中に、引数パラメタの内容($routeArguments)が入ってる」ってのをほんのりと記憶しておきませう。

あと。withAttributeって確か「インスタンスをclone」しているはずなんだよねぇ。あんまりパラメタ数が大きいとオーバヘッドとかかかりそうな気がせんでもないんだけど、その辺、どうなんだろうさね?


       $newResponse = $handler($this->callable, $request, $response, $this->arguments);

ここで呼んでるんだよねぇ。

ってことは基本、呼ばれる各「Controller#action」相当の子は「ResponseInterfaceをreturnしなきゃいけない」って決まりがあるんだな。

        if ($newResponse instanceof ResponseInterface) {
            // if route callback returns a ResponseInterface, then use it
            $response = $newResponse;
        } elseif (is_string($newResponse)) {

或いは

        } elseif (is_string($newResponse)) {
            // if route callback returns a string, then append it to the response
            if ($response->getBody()->isWritable()) {
                $response->getBody()->write($newResponse);
            }
        }

「文字列をreturn」でもまぁOKで、その場合は「その文字列が設定される」と。


んで、最終的にいずれにしても「$responseインスタンス」がreturnされるのか。

で、ここから、Routeインスタンスとしての

vendor/slim/slim/Slim/MiddlewareAwareTrait.php

    public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response)
    {
        if (is_null($this->tip)) {
            $this->seedMiddlewareStack();
        }
        /** @var callable $start */
        $start = $this->tip;
        $this->middlewareLock = true;
        $response = $start($request, $response);
        $this->middlewareLock = false;
        return $response;
    }

での$responseに入るんだな。

さて、逆追いしていこう。


vendor/slim/slim/Slim/Route.php

    public function run(ServerRequestInterface $request, ResponseInterface $response)
    {
        // Finalise route now that we are about to run it
        $this->finalize();

        // Traverse middleware stack and fetch updated response
        return $this->callMiddlewareStack($request, $response);
    }

だったので、ここもまたreturnで返っていくだけ、かな。


ふむ……なんだかんだ「呼ばれまくってる」ので、イメージがつきにくい。

あと「深く潜るほう」に追いかけてったので、戻りが、わかりにくい。

ちと後手に回ったけど、「callスタック」的な見地から、少し、まとめてみやう。色々と省略したりしているので、補助程度に見てくださいませ。

App#get() | App#post() | App#put() | App#delete()
	App#map()
		Route = App#container->get('router')->map($methods, $pattern, $callable)
		Route#setContainer()
		Route#setOutputBuffering()
App#run()
	App#process()
		Router#setBasePath()
		App#dispatchRouterAndPrepareRoute()
			Route = Router#lookupRoute()
			Route#prepare()
			Request#withAttribute
		App#callMiddlewareStack()
			App#seedMiddlewareStack()
			$start(App) = App#tip
			$start(App) -> App#__invoke
				Router = App#container->get('router')
				Request = App#dispatchRouterAndPrepareRoute()
					Router#dispatch()
					Route = Router#lookupRoute
					Route#prepare()
					Request#withAttribute
				Request#getAttribute
				Route = Router#lookupRoute
				Route#run()
					Route#finalize()
						Route#addMiddleware()
					Route#callMiddlewareStack()
						Route#seedMiddlewareStack()
						$start(Route) = Route#tip
						$start(Route) -> Route#__invoke
							Route#resolveCallable
							Handler = Route#container->get('foundHandler')
							Response = Handler()
								call_user_func()
	(Http\Body#write)
	Response#getBody()->write()
	App#finalize()
		ini_set('default_mimetype', '')
		Response#getBody()->getSize()
	App#respond

さて戻ってきて「Http\Body#write」から再開。


vendor/slim/slim/Slim/Http/Body.php

class Body extends Stream
{

}

vendor/slim/slim/Slim/Http/Stream.php

    public function write($string)
    {
        if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) {
            throw new RuntimeException('Could not write to stream');
        }

        // reset size so that it will be recalculated on next call to getSize()
        $this->size = null;

        return $written;
    }

あら「fwrite($this->stream, $string)」なのか。


    public function __construct($stream)
    {
        $this->attach($stream);
    }
    protected function attach($newStream)
    {
        if (is_resource($newStream) === false) {
            throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource');
        }

        if ($this->isAttached() === true) {
            $this->detach();
        }

        $this->stream = $newStream;
    }

ありゃ………渡してる前提なのか。

vendor/slim/slim/Slim/App.php

                $body = new Http\Body(fopen('php://temp', 'r+'));

なるほど。んじゃ理解したので一旦放置。


次。

                $response = $response->withBody($body);
            } elseif ($outputBuffering === 'append') {
                // append output buffer content
                $response->getBody()->write($output);
            }

なので。

responseの「withBody」と「getBody()->write」について、だねぇ。


まずwithBody。

………ない。継承元の親クラスにいた。

vendor/slim/slim/Slim/Http/Message.php

    public function withBody(StreamInterface $body)
    {
        // TODO: Test for invalid body?
        $clone = clone $this;
        $clone->body = $body;

        return $clone;
    }

この場合のthisはResponseだね。


一方のgetBody()->write。

getBodyはやっぱり継承元。

vendor/slim/slim/Slim/Http/Message.php

    public function getBody()
    {
        return $this->body;
    }

body自体は

vendor/slim/slim/Slim/Http/Response.php

    public function __construct($status = 200, HeadersInterface $headers = null, StreamInterface $body = null)
    {
        $this->status = $this->filterStatus($status);
        $this->headers = $headers ? $headers : new Headers();
        $this->body = $body ? $body : new Body(fopen('php://temp', 'r+'));
    }

で入れてた。

ふむここも php://temp なのか。

おあとはwriteなので、処理的には一緒。


ラスト直前、finalize。

vendor/slim/slim/Slim/Appex.php

    protected function finalize(ResponseInterface $response)
    {
        // stop PHP sending a Content-Type automatically
        ini_set('default_mimetype', '');

        if ($this->isEmptyResponse($response)) {
            return $response->withoutHeader('Content-Type')->withoutHeader('Content-Length');
        }

        // Add Content-Length header if `addContentLengthHeader` setting is set
        if (isset($this->container->get('settings')['addContentLengthHeader']) &&
            $this->container->get('settings')['addContentLengthHeader'] == true) {
            if (ob_get_length() > 0) {
                throw new \RuntimeException("Unexpected data in output buffer. " .
                    "Maybe you have characters before an opening <?php tag?");
            }
            $size = $response->getBody()->getSize();
            if ($size !== null && !$response->hasHeader('Content-Length')) {
                $response = $response->withHeader('Content-Length', (string) $size);
            }
        }

        return $response;
    }

「出力している」っぽい箇所がない。

念のため、returnの直前にexitを入れてみる………うん、出てこない。


んじゃ、出力はrespondかしらん。

    /**
     * Send the response to the client
     *
     * @param ResponseInterface $response
     */
    public function respond(ResponseInterface $response)

あたり、だ。

つまりfinalizeで「出力直前」まで準備して、出力はrespondでやってるのか。

では処理を追いかけてみませう。

        // Send response
        if (!headers_sent()) {
            // Headers
            foreach ($response->getHeaders() as $name => $values) {
                foreach ($values as $value) {
                    header(sprintf('%s: %s', $name, $value), false);
                }
            }

            // Set the status _after_ the headers, because of PHP's "helpful" behavior with location headers.
            // See https://github.com/slimphp/Slim/issues/1730

            // Status
            header(sprintf(
                'HTTP/%s %s %s',
                $response->getProtocolVersion(),
                $response->getStatusCode(),
                $response->getReasonPhrase()
            ));
        }

header関数使ってるのか。エスケープしてない………中身はHeadersクラスっぽい………。

ちょろっと寄り道。

vendor/slim/slim/Slim/Http/Headers.php

    public function set($key, $value)
    {
        if (!is_array($value)) {
            $value = [$value];
        }
        parent::set($this->normalizeKey($key), [
            'value' => $value,
            'originalKey' => $key
        ]);
    }
    public function normalizeKey($key)
    {
        $key = strtr(strtolower($key), '_', '-');
        if (strpos($key, 'http-') === 0) {
            $key = substr($key, 5);
        }

        return $key;
    }

あぁ面白い事やってるなぁ。

ただ、値のエスケープとかフィルタリング*1とか、やってないような。


戻って、出力の残り。

        // Body
        if (!$this->isEmptyResponse($response)) {
            $body = $response->getBody();
            if ($body->isSeekable()) {
                $body->rewind();
            }
            $settings       = $this->container->get('settings');
            $chunkSize      = $settings['responseChunkSize'];

            $contentLength  = $response->getHeaderLine('Content-Length');
            if (!$contentLength) {
                $contentLength = $body->getSize();
            }


            if (isset($contentLength)) {
                $amountToRead = $contentLength;
                while ($amountToRead > 0 && !$body->eof()) {
                    $data = $body->read(min($chunkSize, $amountToRead));
                    echo $data;

                    $amountToRead -= strlen($data);

                    if (connection_status() != CONNECTION_NORMAL) {
                        break;
                    }
                }
            } else {
                while (!$body->eof()) {
                    echo $body->read($chunkSize);
                    if (connection_status() != CONNECTION_NORMAL) {
                        break;
                    }
                }
            }
        }

なんか丁寧な出力の仕方だなぁ。

まぁ極論でいうと「echoで出力している」まる。


ふむ、とりあえず「頭からケツまで」の大体の流れは見えてきたかなぁ。

次は

・slim/slim-skeleton の解析(含む autoloader 周り):「やってること」と「できる事」と「多分公式が推奨してるんじゃないかと思われる方向性」の確認

https://www.slimframework.com/docs/ のめぼしいところをつまみ食い

 → Middlewareの確認(と、必要そうなら解析)、ルーティングのgroups

 → jsonの出力

 → validate

あたり。つまみ食い関連は、上述以外でも面白そうなのがあれば、色々。


この辺が一通り落ち着いたら

・どんな風に書くか:おいちゃん流

を少しづつ作っていって、頑張れそうなら

・MagicWeaponとの悪魔合体

を試みてみませうw

*1:いらん値の除去、くらいのニュアンス